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 Exception
s 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 anException
- The BGW's
RunWorkerCompleted
event catches theException
, logs and does some cool cleanup work. - After cleanup up, the
RunWorkerCompleted
event re-throws theException
.
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");
}
"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.