In .NET exception handling has become a much simpler issue than in previous programming languages. For example, in many older languages it was considered good practice to put an exception handler in every routine, and also to return a value to indicate success or failure from each routine.
In .NET Windows Forms applications these techniques are no longer relevant, although for many developers this seems to be a well-guarded secret. In particular it is now possible (and considered good practice) to set up a top-level exception handler that will handle any unexpected exception on the main thread in a Windows application. This means that it is no longer necessary to have exception handlers in every routine.
This article will describe how to set up this top-level handler, and will discuss some of the issues in making it work effectively.
So what happens if an exception occurs in our application and we don’t handle it?
In the development environment, the code will break and we’ll get a fairly friendly ‘Exception was unhandled’ dialog which allows us to investigate the exception.
However in a release build we’ll get the dialog below. We don’t really want our users wrestling with that.
Things get even worse if we run code on a background thread and an unhandled exception occurs there. We then get the dialog below if we are not running in debug. Quite why Microsoft would want to know about our threading bugs is a little beyond me, however.
So if we’re not putting exception handlers in all our routines how do we stop all this from happening? The answer is that Microsoft has provided us with a great mechanism that allows us to set up one exception handler that is hooked up at system start up and can handle all exceptions from the main thread in our code. We no longer have to write reams of boilerplate code to handle default exception handling scenarios.
As discussed, for a Windows Forms application it is possible to set up an exception handler that catches any exception that occurs on the main GUI thread. To do this we need to handle the ThreadException event as shown in the attached code listing 1. Note that we need to hook up the handler for the ThreadException event BEFORE the Windows message loop is set up. In practice this means we need the line of code below before any form (including a splash screen) is displayed. If you make it the first line of code in your Main() method then you’ll find it works.
Application.ThreadException += new ThreadExceptionEventHandler(new ThreadExceptionHandler().ApplicationThreadException);
Here Application.ThreadException is an event in System.Windows.Forms, whilst ThreadExceptionEventHandler is the associated delegate in System.Threading. ThreadExceptionHandler is our own class that will handle all exceptions, and ApplicationThreadException is the method in it that does the handling.
If you set up and run the example code you will see that a form is shown with two buttons that can be clicked. The first throws an exception, whilst the second shows a new instance of the form. Whenever you click the ‘Throw Exception’ button on any form the exception generated is caught by our top-level exception handler.
Handling Exceptions in Debug
Furthermore, if you run the project in debug in Visual Studio the code will break immediately when the exception is thrown at the point that it is thrown. This is exactly the behaviour you want in general. If you’ve got an exception when developing you want to be able to see immediately what’s happened so that you can fix it. With other exception handling scenarios you are often floundering around trying to recreate the exception with breakpoints in appropriate places.
Note that if you want your top-level exception handler to execute after the code has broken in debug you can just hit F5 to continue. If you don’t want your code breaking on exceptions in debug at all, you can disable this behaviour in Visual Studio 2005 by going to the Exceptions option on the Debug menu and clearing the ‘Break when exception is:’ ‘User-unhandled’ checkbox under ‘Common Language Runtime Exceptions’. If you expand Common Language Runtime Exceptions you can enable or disable this behaviour for individual exception types.
Getting the ThreadException Event to Fire Correctly
It should be straightforward to get the ThreadException event firing in code listing 1. This is a very simple example after all. In practice getting this working can prove a little tricky. There are two rules to remember:
1. The handler needs to be set up BEFORE the Windows message loop is constructed.
This means it needs to be before Application.Run(MainForm). If you are showing a splash screen at start up, or even a ‘Do you want to upgrade to the new version?’ form before calling Application.Run, you need to put your Application.ThreadException += new ThreadExceptionEventHandler(… statement before the code to do those things is called.
2. The handler is not in force until the initialization of your main form has completed.
Any exception in the constructor of the main form, or in any code you execute prior to Application.Run(MainForm) will not be handled by the top-level handler. Of course, putting complex initialization code in the constructor of a form is a bad idea in general, not least because it gets run by the designer when you open the form in Visual Studio. However, this does mean that you may need a try..catch block around any initialization code that runs before the main form is launched in case it throws an exception.
Finally don’t forget that by default in debug you get the same ‘Exception was unhandled’ dialog whether the ThreadException event is hooked up correctly or not. It’s what happens when you hit F5 (or disable the ‘Break when exception is user-unhandled’ options) that tells you whether you’ve got it working – see the paragraph above.
Examples of how to handle these rules are given below.
Example 1: Getting the ThreadException to Fire Correctly with a Splash Screen
Consider the start up code in code listing 2.
The ThreadException handler will NOT work in this scenario. To fix this move the set up code for the handler to be the first line in routine Main().
See code listing 3.
But remember that if DoLongRunningStuff() throws an exception it will NOT be caught by the top-level handler. To avoid unhandled exceptions at start up you may want to put a try..catch block around the start up code.
Example 2: Handling Exceptions Correctly with Main Form Initialization Code
You may have some initialization code you want to run in your MainForm class. As discussed, it’s not a good idea to put that in the constructor, not least because the ThreadException handler won’t catch any exceptions thrown in it. It’s better to have a separate Initialize() routine in general. But how do we call this so we have an exception handler in place? One approach is to re-use the try..catch block set up in Example 1 above.
See code listing 4.
We now have a splash screen displayed throughout the time our initialization code is running, with an exception handler in place. All subsequent exceptions will be caught by our top-level exception handler.
Handling the Exception
For the example code our actual exception handler (ThreadExceptionHandler().ApplicationThreadException) simply shows a message box. For production code you will probably want a more sophisticated exception handler than this.
Close the Application?
Our top-level handler leaves the Windows message loop for the application running normally, assuming it can do so. So if an exception occurs in a routine you’ll get a sensible exception message, some options for dealing with it, and can then carry on using the system. In spite of arguments to the contrary in some blogs, I think that in a Windows Forms application this is normally the behaviour you’ll want. After the first release of a system (and assuming you’ve got appropriate levels of automated testing) it’s quite rare for a bug to get through that’s going to bring the entire application to its knees. Exceptions tend to occur because developers have forgotten basic scenarios, for example attempting to save a file that’s locked because it’s open, or handling a null field in a DataSet correctly. You really don’t want the user to have to restart the application just because they attempted to save a file they already had open. If a bug does get through that leaves the application unusable our user can always shut it down themselves: most users are accustomed to the standard ‘restart to fix the problem’ scenario.
Exceptions in Threads other than the Main GUI Thread
Note that the ThreadException event handler will NOT catch exceptions thrown on any threads other than the GUI thread. I won’t discuss this in detail here, but in general it makes sense to have specific exception handlers in the individual threads that deal with any exceptions in those threads. There is also an UnhandledException event that can be used to set up a top-level exception handler for thread exceptions. This works very much like the ThreadException event described here, the difference being it handles exceptions on threads other than the GUI thread. However, if there is an exception on a non-GUI thread the application will close after running the top-level handler. It is not possible to stop this happening: as mentioned we are better off trying to handle exceptions on the threads that generate them.