Why can an Exception not be rethrown in the BackgroundWorker RunWorkerCompleted event

931 views Asked by At

I've checked through some posts on this topic which cover how to get around this issues, but I can't quite understand the Why of this odd behaviour?

Short version:

Why is are Exceptions thrown inside the RunWorkerCompleted event not caught by the calling code?

Detailed version:

  • I have a BackgroundWorker (BGW from now on).
  • The BGW's DoWork event throws an Exception
  • The BGW's RunWorkerCompleted event catches the Exception, logs and does some cool cleanup work.
  • After cleanup up, the RunWorkerCompleted event re-throws the Exception.

If the RunWorkerCompleted event runs on the Main thread, wouldn't that mean that the calling code (also on the main thread) should be able to catch that exception?

Some code to reinforce the concept...

private void SomeMethod()
{
    BackgroundWorker bgw = new BackgroundWorker();
    bgw.DoWork += bgw_DoWork;
    bgw.RunWorkerCompleted += bgw_RunWorkerCompleted;
    bgw.RunWorkerAsync();
}

private void bgw_DoWork(object sender, DoWorkEventArgs e)
{
    throw new Exception("Oops");
}

private void bgw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    if(e.Error != null)
    {
        // Log exception & cleanup code here...
        throw e.Error; // Always unhandled :(
    }
}

I would assume that calling SomeMethod like this would would catch the Exception and show the Message Box, but the BGW doesn't behave as I expected and the Exception always remains unhandled...

try
{
    SomeMethod();
}
catch (Exception)
{
    MessageBox.Show("Handled exception");
}
2

There are 2 answers

0
Hans Passant On BEST ANSWER

... not caught by the calling code?

"Calling code" is the important detail in your question, who exactly calls your RunWorkerCompleted event handler? You know it cannot be your code, you never wrote any that explicitly calls the event handler. All you did was subscribe the event. So you know there's trouble looming there, since it wasn't your code, you cannot possible write a try/catch to catch that exception and handle it.

I strongly recommend you just have a look, set a breakpoint on your event handler and, when it breaks, look at the debugger's Call Stack window to see how it got there. Keep in mind that this will be .NET Framework code, you'll want to turn off the Just My Code debugger option so you can see everything.

Uncommon cases first, in a console app or service it was SychronizationContext.Post() that called the event handler. Code runs on an arbitrary threadpool thread and there isn't any catch statement that can catch this exception. Your app will always terminate.

A common case is Winforms, the last statement you see in the Call Stack window that had anything to do with your code is the call to Application.Run() in your Main() method. Your event handler was triggered by a call to Control.BeginInvoke(), you can't see it, but that's how your event handler code ended up running on the UI thread. Winforms has a backstop catch statement in its Run() method that catches and that will raise the Application.ThreadException event. It is only active when you don't use a debugger. If you didn't otherwise subscribe your own event handler, the default handler displays the ThreadExceptionDialog dialog, the one that gives the user the option to click Continue or Quit. Not a great idea to ever let it get that far, your user has no decent way to pick the right choice, other than by trial and error. Do beware the role the debugger plays, it will work differently without one and the ThreadException event is raised directly.

Next common case is WPF, it works pretty similar to Winforms and your event handler was triggered by a call to Dispatcher.BeginInvoke(). It also has a catch statement in its dispatcher loop. And similarly, it raises the DispatcherUnhandledException event. It does not have a default handler for that event like Winforms does, if you don't subscribe one then the app will terminate.

So, in general, the somewhat inevitable outcome is that your app is going to terminate when the re-raise the exception. There just isn't any fairy godmother around that wants to handle the problem you didn't want to handle. Handling exceptions like this is in general rather questionable, you have very little idea what actually went wrong in your DoWork event handler. It is a given that it could not handle the exception, otherwise it would have caught it. The odds that you can do it later and do it correctly dwindle rapidly the further the catch is removed from the statement that threw.

All you really know in your RunWorkerCompleted event handler is that "it did not work". Handling such exceptions is risky, you have no idea how much of your program state got mutated by the failing code. There is a big advantage though, the kind of code you put in DoWork is by nature very loosely coupled. Very important, tightly coupled code that runs on a worker thread is almost impossible to write, you can rarely get the required locking correct.

In practice you perform a single operation that does not mutate any state at all. Like a dbase query that fills a list with query results. You use the e.Result property in your event handler to apply the result of the background operation. So no real harm done when it did not work, you just didn't get a result. You do have to let the user know about it, after all he did not get the result he expected. And frequently the user has to call somebody to get the problem corrected, hopefully not you but IT staff that fixes the underlying problem. MesssageBox.Show() gets that job done.

1
Niels Filter On

I figured it out while proof reading my own question. I'll share anyway, hopefully helping someone else. The answer lies somewhere between the RunWorkerAsync() method and my silly misconception.

RunWorkerAsync() is (as it clearly states) an async method, so it doesn't block or wait. This means that the Main Thread is free to continue and therefore exits past the try/catch even though the BGW's DoWork might still be busy.

When the BGW's DoWork finally throws an Exception, the RunWorkerCompleted event is no longer in the scope of the calling code's try/catch.

It wouldn't actually make sense to catch it in the calling code. Allowing it to be caught by the caller would cause an unstable race condition, where sometimes it would be caught and other times it would be too late and unhandled.