Gotta catch 'em all: Last-chance exception handling in .NET with WinForms
Posted on Thu 07 March 2013 in blog
Recently, I went through the exercise of hooking up a crash-reporting component to a large .NET application using Windows Forms. The goal, of course, is to catch all unhandled exceptions so they can be reported to the developer.
Throughout this post I'll be referring to this Program.cs
:
The code will be incrementally un-commented for each of the examples. I'll also link to compiled example executables. If you don't trust my binaries, you can compile them yourself.
Exception-handling progression
0. No exception handling
First we see an application that throws exceptions in the UI thread and a
background thread, with no handling. Try out 0_Nothing.exe
.
With no exception handling, background thread exceptions crash hard. UI thread exceptions are handled by the built-in .NET WinForms handler:
This dialog has a Continue option which allows the user to ignore the exception and go on. This method is absolutely unacceptable. No exceptions should ever be allowed to be ignored, as the program is in an indeterminate state.
1. try / catch
The naive approach would be to set up a try
/catch
block in Main()
around
the Application.Run()
call. See 1_TryCatch.exe
.
try {
Application.Run(new Form1());
}
catch (Exception ex) {
// ...
}
We see here that there is no difference between this and the version with no
try
/catch
. This is because the UI thread exceptions are still being handled
inside of Application.Run()
by the default handler. The try
/catch
is
never used, and background thread exceptions are unaffected.
2. Application.ThreadException
Next, we utilize WinForms' Application.ThreadException
event to handle UI thread exceptions. See
2_Application_ThreadException.exe
.
Application.ThreadException += (sender, args) =>
HandleException("Application.ThreadException", args.Exception);
Here, we see that instead of the unacceptable WinForms handler, our handler was called (for UI thread exceptions). However, as MSDN points out:
This event allows your Windows Forms application to handle otherwise unhandled exceptions that occur in Windows Forms threads.
...
To catch exceptions that occur in threads not created and owned by Windows Forms, use theUnhandledException
event handler.
So background thread exceptions still crash hard in this example.
3. AppDomain.UnhandledException
Now we follow the documentation and hook up the
AppDomain.UnhandledException
handler. See
3_AppDomain_UnhandledException_NoUhandledMode.exe
.
AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
HandleException("AppDomain.UnhandledException", (Exception)args.ExceptionObject);
Now finally, we are able to catch exceptions on background threads with this
handler. UI thread exceptions, however, are still handled by our
Application.ThreadException
handler.
4. Application.SetUnhandledExceptionMode
As mentioned in the MSDN documentation, a call to
Application.SetUnhandledExceptionMode
and passing
UnhandledExceptionMode.ThrowException
tells Winforms to not use the Application.ThreadException
handler.
Instead, it lets exceptions bubble out of Application.Run
.
See this in 4_Everything.exe
.
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.ThrowException);
The result is that the try/catch around Application.Run actually works now: UI thread exceptions are now caught by that handler.
5. No more try / catch
Finally, removing the try
/catch
around Application.Run
allows for all
unhandled exceptions to be handled via AppDomain.UnhandledException
. See
5_Final.exe
. This is how we ended up handling everything in our
application; we found it ideal to have one route for all
unhandled exceptions...
In fact it gets even more complicated. There are certain scenarios where
exceptions that need to cross Kernel or COM boundaries can be swallowed. For
example, the Form.OnLoad
method is actually a user-mode kernel callback.
These are notorious for swallowing exceptions. In cases where we
are sufficiently suspect of exceptions, we set up a try
/catch
and manually
hand off the exception to the common handler.
Summary
It is important to note that all exceptions are handled on the thread that they
occurred on. If you're in the same boat I was in, you're stuck with a
third-party crash handler component that had to be run on the UI thread.
Because of this, I marshal the calls to the UI thread with a call to
Control.Invoke()
, as usual for cross-thread UI stuff.
The full source code for my example binaries can be downloaded here.