17

I'm seeing some wierd behaviour when throwing exceptions and catching them in the Application.ThreadException event handler.

Basically whats happening in the sample below is that an exception is thrown in the DoWork event handler of a BackgroundWorker. The RunWorkerCompleted event handler rethrows a new exception with the original as the inner exception.

Why does the inner exception show up in the ThreadException event handler and not the acutal exception being thrown? If I do not provide an inner exception in the RunWorkerCompleted event handler, the correct exception will show up.

using System;
using System.Windows.Forms;
using System.ComponentModel;

namespace WierdExceptionApp
{
    class WierdExceptionForm : Form
    {
        BackgroundWorker worker = new BackgroundWorker();

        public WierdExceptionForm()
        {
            worker.DoWork += new DoWorkEventHandler(worker_DoWork);
            worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(worker_RunWorkerCompleted);
            worker.RunWorkerAsync();
        }

        void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            if (e.Error != null)
            {
                throw new Exception("worker_RunWorkerCompleted", e.Error);
            }
        }

        void worker_DoWork(object sender, DoWorkEventArgs e)
        {
            throw new Exception("worker_DoWork");
        }

        [STAThread]
        static void Main()
        {
            Application.ThreadException += new System.Threading.ThreadExceptionEventHandler(Application_ThreadException);
            Application.Run(new WierdExceptionForm());
        }

        static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e)
        {
            MessageBox.Show(e.Exception.Message);
        }
   }
}
Mitch Wheat
  • 295,962
  • 43
  • 465
  • 541
Magnus Lindhe
  • 7,157
  • 5
  • 48
  • 60
  • Astounding; this has been biting me for years without me realizing it. I'd see these occasional error reports for things that I could swear I handled. They were rare enough to not spend a lot of time looking into them, but finally it started happening with something significant, and I realized the exception was changing along the way. WTF?? – Mark Sowul Apr 23 '11 at 03:57

2 Answers2

13

The RunWorkerCompleted event is marshaled from the BGW thread to the UI thread by the WF plumbing that makes Control.Invoke() work. Essentially, there's a queue with delegates that is emptied by the message loop. The code that does this, Control.InvokeMarshaledCallbacks(), you'll see it on the call stack, has a catch (Exception) clause to catch unhandled exceptions. That clause calls Application.OnThreadException, passing the value of Exception.GetBaseException().

Well, that explains why you only see the inner exception. Why it is done this way is a bit unclear. Possibly to slice off the stack frames of the code in the UI thread that are otherwise pretty confusing since the real exception came from the background thread.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • 10
    Yet *another* bug in WinForms that Microsoft most certainally never fix! – Joshua Dec 07 '08 at 22:12
  • 2
    How stupid is that! MS will certainly never fix this as it would be a "breaking change" in their eyes. Still lets all go to https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=433765 and register the issue anyway. – Martin Brown Apr 20 '09 at 16:06
  • Trying to figure this out further here: https://stackoverflow.com/q/59146505/7453 – Patrick Szalapski Dec 02 '19 at 21:02
  • I don't think it uses `Exception.GetBaseException()` because when I override that in my exception class `public override Exception GetBaseException() => this;` the `ThreadException` handler still gets the inner exception. – blackboxlogic Oct 19 '21 at 15:42
  • 1
    https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/Control.cs,7495 – Hans Passant Oct 19 '21 at 15:51
  • I see. I'm in a slightly different context because I'm using async on the UI thread instead of a background worker. So I don't think I'm going through the marshaling code you linked but I landed here because I'm sure seeing the same undesired behavior in the ThreadException handler. – blackboxlogic Oct 19 '21 at 19:11
1
        if (e.Error != null)
        {
            throw new Exception("worker_RunWorkerCompleted", new Exception("Inner", new Exception("Inner inner")));
        }

You get "inner inner" at the end. It seems that this is the behavior of Application_ThreadException method to look at the inner-most exception.

liggett78
  • 11,260
  • 2
  • 29
  • 29