Introduction
One of the things that impressed me when I first started learning .NET was
its enhanced exception-handling functionality. By this I mean such features as
easy access to the type of exception thrown, full stack trace and inner
exceptions. This makes it easy to still get full information in those places
where you just catch a top-level System.Exception
. I find this convenient since,
if you don't need to take specific action on a particular type of exception, it
is tedious to have successive catch handlers for each type of exception that may
be thrown. In any case, even if you do catch specific exceptions you usually
also need to catch System.Exception
just to cover yourself and prevent program
crashes. Thus I find that I end up catching System.Exception
all over the place
in my code. A typical scenario is that in Windows Forms and ASP.NET Web Forms
applications, all of my non-trivial event handlers end up containing try-catch
System.Exception
blocks.
The trouble is that this does still clutter up the code somewhat and doesn't
really seem quite right. Is there a better way?
The better way
A couple of months ago someone posted a message on one of Microsoft's .NET
newsgroups asking just this question. In having catch System.Exception
all over
the place what you're really doing is a "catch-all," i.e., you're
trying to catch unhandled exceptions - exceptions that you don't know
about.
One of Microsoft's developers responded to the post and pointed out that we
should not use exception blocks to catch unhandled exceptions. Instead,
depending on our type of application, Console, Windows or ASP.NET, we should
use one of .NET's unhandled exception event handlers. This means that we'll just
have one error handler to handle all unhandled exceptions. When such an
exception is generated we can provide the user with the option of continuing or
aborting the application.
This is documented in the .NET Help but it doesn't really stand out in the
various exception
handling topics. You can find some discussion for ASP.NET in the
article, Exception Management in .NET.
In the discussion that follows I describe how to manage
unhandled exceptions in Windows Forms applications.
Consider a simple Windows Forms application containing a single form with two
buttons and a text box. The discussion is in C# but a full example follows in
both C# and Visual Basic .NET.
The Add
button just adds my name to the text box. The Remove
button clears
it. Suppose the Add
button throws an exception. Hitherto, I would have done
something like this.
private void btnAdd_Click(object sender, System.EventArgs e)
{
try
{
txtName.Text = "Kevin";
throw new InvalidOperationException("Invalid operation.");
}
catch (System.Exception ex)
{
DisplayError(ex);
}
}
(Normally, I would only do this if the Add
function were performing some
elaborate operation - typically calling other non-trivial routines - but I'm here just illustrating the
process.)
But by writing an unhandled exception event handler delegate we can dispense
with the try-catch block above. The signature for the delegate looks like this.
public static void Application_ThreadException(
object sender, ThreadExceptionEventArgs e)
{
}
And this is how we hook it up.
static void Main()
{
Application.ThreadException += new ThreadExceptionEventHandler(
Application_ThreadException);
Application.Run(new Form1());
}
So the Add function can now look like this.
private void btnAdd_Click(object sender, System.EventArgs e)
{
txtName.Text = "Kevin";
throw new InvalidOperationException("Invalid operation.");
}
Example
Here is the complete example in C# and Visual Basic .NET (with Windows Form designer generated code
omitted). I have also moved the exception-handling event into an assembly-wide
class so that it can be accessed by other forms. When an unhandled exception is
received an AbortIgnoreRetry
dialog is displayed giving a full description of
the error. Of course, in a production version we would just inform the user of
an application error and log the details.
C# Implementation
using System;
using System.Threading;
public class Form1 : System.Windows.Forms.Form
{
[STAThread]
static void Main()
{
ThreadExceptionHandler handler =
new ThreadExceptionHandler();
Application.ThreadException +=
new ThreadExceptionEventHandler(
handler.Application_ThreadException);
Application.Run(new Form1());
}
private void btnAdd_Click(object sender, EventArgs e)
{
AddWithUnhandledException();
}
private void btnRemove_Click(object sender, EventArgs e)
{
txtName.Clear();
}
private void Add()
{
try
{
txtName.Text = "Kevin";
throw new InvalidOperationException(
"Invalid operation.");
}
catch (System.Exception ex)
{
DisplayError(ex);
}
}
private void AddWithUnhandledException()
{
txtName.Text = "Kevin";
throw new InvalidOperationException(
"Invalid operation.");
}
private void DisplayError(Exception ex)
{
MessageBox.Show(ex.GetType() + "\n\n" +
ex.Message + "\n\n" +
ex.StackTrace,
"Error",
MessageBoxButtons.AbortRetryIgnore,
MessageBoxIcon.Stop);
}
}
internal class ThreadExceptionHandler
{
public void Application_ThreadException(
object sender, ThreadExceptionEventArgs e)
{
try
{
DialogResult result = ShowThreadExceptionDialog(
e.Exception);
if (result == DialogResult.Abort)
Application.Exit();
}
catch
{
try
{
MessageBox.Show("Fatal Error",
"Fatal Error",
MessageBoxButtons.OK,
MessageBoxIcon.Stop);
}
finally
{
Application.Exit();
}
}
}
private DialogResult ShowThreadExceptionDialog(Exception ex)
{
string errorMessage=
"Unhandled Exception:\n\n" +
ex.Message + "\n\n" +
ex.GetType() +
"\n\nStack Trace:\n" +
ex.StackTrace;
return MessageBox.Show(errorMessage,
"Application Error",
MessageBoxButtons.AbortRetryIgnore,
MessageBoxIcon.Stop);
}
}
Visual Basic .NET Implementation
Imports System.Threading
Public Class Form1
Inherits System.Windows.Forms.Form
Private Sub Form1_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
Dim handler As ThreadExceptionHandler = _
New ThreadExceptionHandler()
AddHandler Application.ThreadException, _
AddressOf handler.Application_ThreadException
End Sub
Private Sub btnAdd_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnAdd.Click
Add()
End Sub
Private Sub btnRemove_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnRemove.Click
txtName.Clear()
End Sub
Private Sub Add()
Try
txtName.Text = "Kevin"
Throw New InvalidOperationException( _
"Invalid operation.")
Catch ex As System.Exception
DisplayError(ex)
End Try
End Sub
Private Sub AddWithUnhandledException()
txtName.Text = "Kevin"
Throw New InvalidOperationException( _
"Invalid operation.")
End Sub
Private Sub DisplayError(ByVal ex As Exception)
MessageBox.Show(ex.GetType().ToString() & _
vbCrLf & vbCrLf & _
ex.Message & vbCrLf & vbCrLf & _
ex.StackTrace, _
"Error", _
MessageBoxButtons.AbortRetryIgnore, _
MessageBoxIcon.Stop)
End Sub
End Class
Friend Class ThreadExceptionHandler
Public Sub Application_ThreadException( _
ByVal sender As System.Object, _
ByVal e As ThreadExceptionEventArgs)
Try
Dim result As DialogResult = _
ShowThreadExceptionDialog(e.Exception)
If (result = DialogResult.Abort) Then
Application.Exit()
End If
Catch
Try
MessageBox.Show("Fatal Error", _
"Fatal Error", _
MessageBoxButtons.OK, _
MessageBoxIcon.Stop)
Finally
Application.Exit()
End Try
End Try
End Sub
Private Function ShowThreadExceptionDialog( _
ByVal ex As Exception) As DialogResult
Dim errorMessage As String = _
"Unhandled Exception:" _
& vbCrLf & vbCrLf & _
ex.Message & vbCrLf & vbCrLf & _
ex.GetType().ToString() & vbCrLf & vbCrLf & _
"Stack Trace:" & vbCrLf & _
ex.StackTrace
Return MessageBox.Show(errorMessage, _
"Application Error", _
MessageBoxButtons.AbortRetryIgnore, _
MessageBoxIcon.Stop)
End Function
End Class
Console and ASP.NET Applications
For Console applications you should use the
System.AppDomain.UnhandledException event. To hook it up you would write:
Thread.GetDomain().UnhandledException += new
UnhandledExceptionEventHandler(Application_UnhandledException);
And the event handler looks like this.
public static void Application_UnhandledException(
object sender, UnhandledExceptionEventArgs e)
{
}
For ASP.NET applications you use the System.Web.HttpApplication.Error event
which is placed in the Global.asax file. This might look something like:
protected void Application_Error(Object sender, EventArgs e)
{
Exception ex = Server.GetLastError();
Context.ClearError();
Response.Write("Application_Error");
Response.Write("Error Message: " + ex.ToString());
}