Introduction
In Visal Basic 6, there was no way to globally handle all of the errors in
your application. You could simulate one by wrapping every sub or
function in an On Error
statement, but that was rather painful.
One of the great features of .NET is the very
powerful thread and application level exception handling mechanisms that
make developing a global error handler practical. One of the first things I set
out to do when starting with .NET was finally implement all those great global
error handling ideas I had considered, but had to abandon in the last 5 years of
working with VB5 and VB6. I found an excellent article on Code Project
documenting the basic
techniques of unhandled exception handling in .NET, so armed with that, I
set out to implement my "dream" error handling system.
At the time of the unhandled exception, my
UnhandledExceptionHandler
class will:
- Display a user-friendly message
- Capture a screenshot of the user's desktop
- Build a string containing all the diagnostic information possible
- Notify the developers via SMTP e-mail, and include the screenshot
- Write an event to the event log
- Write a text log in the application folder
About Unhandled Exceptions
The great thing about a good global exception
handler is that it makes the rest of your code easier to read. Rather than
cluttering up your code with
Try..Catch
blocks for every possible
error condition, no matter how rare, you can simply rely on the solid, built-in
global handler to deal with those oddball scenarios-- and to notify you when
they happen!
So then the natural question that most developers ask is, "When should I
catch exceptions"? And it's a very good question. Here are some guidelines that
I have found useful.
- Unless you have a very good reason to catch an exception, DON'T.
Exceptions are supposed to be exceptional, just like the dictionary meaning:
uncommon, unusual. When in doubt, let the calling routine, or
the global exception handler, deal with it. This is the golden rule. The
hardest kinds of exceptions to troubleshoot are the ones that don't even
exist, because a developer upstream of you decided to consume it.
- If you can correct the problem implied by the exception. For
example, if you try to write to a file and it is read-only, try removing the
read-only flag from the file. In this case you handled the exception and fixed
the problem, so you should eat the exception. It doesn't exist, because you
fixed it.
- If you can provide additional information about the exception. For
example, if you fail to connect via HTTP to a remote website, you can provide
details about why the connection failed: was the DNS invalid? Did it
time out? Was the connection closed? Did the site return 401 unauthorized,
which implies that credentials are needed? In this case you want to catch the
exception, and re-throw it as an inner exception with more information. This
is a very good reason to catch an exception, but note that we are still
re-throwing it!
- Always try to catch specific exceptions. Avoid catching
System.Exception
whenever possible; try to catch just the
specific errors that are specific to that block of code. Catch
System.IO.FileNotFound
instead.
There are, of course,
times when you'll want to violate these rules for completely legitimate
reasons-- but at least consider them before you do.
Background
This approach leverages two specific exception handlers. The
Application.ThreadExceptionHandler
, which works for the main
application thread of a Windows Forms application:
AddHandler Application.ThreadException, AddressOf ThreadExceptionHandler
And the AppDomain.CurrentDomain.UnhandledException
handler,
which works for Console applications:
AddHandler System.AppDomain.CurrentDomain.UnhandledException, _
AddressOf UnhandledExceptionHandler
This leaves out one large class of .NET applications: web apps. I've
experimented with this, and I do not believe it is desirable to handle web
exceptions using the exact same classes that you use to handle Console and
WinForms exceptions. They have different needs. I do have a variant of this
class I use in server-side Web Services and ASP.NET applications, but it's
significantly different. This article will only cover exception handling
techniques appropriate for Console or Windows Forms executables.
The other thing we need to be aware of is multi-threaded applications. The
default exception handling only covers the main application thread. If you are
spawning additional threads in your application, you must remember to set up
the unhandled exception handler for each new thread you spawn.
Using UnhandledExceptionManager
Setting up the unhandled exception handler is very easy, but it must be done
as soon as possible at application startup by calling
UnhandledExceptionManager.AddHandler()
For Windows Forms applications:
Public Sub Main()
UnhandledExceptionManager.AddHandler()
Dim frm As New Form1
Application.Run(frm)
End Sub
For Console applications:
Sub Main()
UnhandledExceptionManager.AddHandler(True)
Console.WriteLine("Hello World!")
DoSomeWork()
End Sub
This is a "set it and forget it" operation. Once you've hooked up the
handler, everything else is automatic from this point on. The only
difference between the two initialization routines is the single optional
Boolean parameter blnConsoleApp
. Unfortunately I couldn't determine
any way to distinguish between a Console and a Windows Forms app at runtime; the
distinction is important for reasons I'll cover later.
The UnhandledExceptionManager
class can be explicitly configured
at initialization time through its parameters, or it can be configured passively
through ambient App.Config settings. The App.Config settings are all optional,
but look like this (I chose all defaults):
<add key="UnhandledExceptionManager/EmailTo"
value="jatwood@spamnotcodinghorror.com" />
<add key="UnhandledExceptionManager/ContactInfo"
value="Jeff Atwood at 123-555-1212" />
<add key="UnhandledExceptionManager/IgnoreDebug" value="True" />
<add key="UnhandledExceptionManager/SendEmail" value="True" />
<add key="UnhandledExceptionManager/LogToFile" value="True" />
<add key="UnhandledExceptionManager/LogToEventLog" value="False" />
<add key="UnhandledExceptionManager/TakeScreenshot" value="True" />
<add key="UnhandledExceptionManager/DisplayDialog" value="True" />
<add key="UnhandledExceptionManager/EmailScreenshot" value="True" />
<add key="UnhandledExceptionManager/KillAppOnException" value="True" />
The most important setting is DisplayDialog
, which defaults to
True. For a Windows Forms app, when an Unhandled Exception occurs, we generate a
user-friendly error dialog based on the GUI design laid out by Alan Cooper in his book
About
Face: The Essentials of User Interface Design, in the chapter titled The
End of Errors.
To protect the end user from "geek speak", the Exception dialog hides most of
the detailed diagnostic information until the "More >>" button is clicked.
The animated screenshot at the top of this article demonstrates the use of this
button.
Of course in a console app, we can't throw up a traditional WinForms dialog,
so we simply dump the text to the console, like so:
But the really cool thing about the UnhandledExceptionHandler
class is that, in addition to putting a friendly UI 'face' on our exceptions, it
also does a lot of things for us behind the scenes. First of all, it takes a
screenshot of the exception and logs the exception to a text file. These files
are stored in the same path as the executable. Note that, for Smart Client apps
which have a remote URL for a path, these files are named with filesafe versions
of the URL and stored in the root folder of the hard drive.
By default, UnhandledExceptionHandler
bundles up the text log
and the screenshot into an exception email. This information is emailed to the
address(es) of your choice via the built in, native SMTP class
SimpleMail
, assuming you've configured it with a valid SMTP server
address. The email contains:
- User and Machine identification
- Application information
- Exception summary
- Custom stack trace
- .PNG desktop screenshot
As you can see, the screenshot that was captured to the .PNG file is attached
directly to the email. The screenshot is taken immediately after the Unhandled
Exception, but before any UI appears; this way we capture the "scene of the
crime", and hopefully have some idea what the user was doing to trigger the
exception (damn users!). This provides additional valuable information that
sometimes isn't reflected in the stack trace.
Also note that the email from: address, pictured above, defaults to
Filename@Machine.Domain.com, a naming scheme I'm arrived at after a few
years of standard email notifications; this way you know "who" mailed you (what
application), and where it is coming from (what computer).
But wait-- there's more! I don't frequently enable this in my apps, but the
code can also write the same detailed exception information to the Application
Event Log, if you'd like to see them there.
Two final observations on Unhandled Exceptions before we close our
discussion.
- For some reason, using an Unhandled Exception Handler can block
termination of your WinForms app. In other words, after the Unhandled
Exception is processed, your application.. keeps running. I'm not clear
exactly why this happens, and I'm not even sure if this is a desirable
"feature", as you can get into some very strange execution states when the
application partially fails, but keeps running. To control this, I added the
UnhandledExceptionManager.KillAppOnException
property, which
defaults to True. Set it to taste, it works either way.
- Bear in mind that you cannot have exceptions in the Unhandled Exception
Handler. This is a special case handler, the "handler of last resort", and
exceptions in this code are just not supported-- they will cause the code to
terminate with no warning whatsoever, as if you put in an arbitrary
Return
right smack dab in the middle of your function. Beware!
Using HandledExceptionManager
That pretty much covers Unhandled Exceptions, which is great, but.. what
about exceptions that we want to handle? In other words, situations that
we know can happen, and we want to specifically protect the user from-- without
terminating our application? For that, I included
HandledExceptionManager
, which leverages the same UI. It's like a
MessageBox specifically designed for exceptions, that supports an
optional email notification.
The syntax is very similar to MessageBox
, in fact:
Public Overloads Shared Function ShowDialog(ByVal strWhatHappened As String, _
ByVal strHowUserAffected As String, _
ByVal strWhatUserCanDo As String, _
ByVal objException As System.Exception, _
Optional ByVal Buttons As MessageBoxButtons = MessageBoxButtons.OK, _
Optional ByVal Icon As MessageBoxIcon = MessageBoxIcon.Warning, _
Optional ByVal DefaultButton _
As UserErrorDefaultButton = UserErrorDefaultButton.Default) _
As DialogResult
But it can also be called using an even simpler overloaded method.
Public Overloads Shared Function ShowDialog(ByVal strWhatHappened As String, _
ByVal strHowUserAffected As String, _
ByVal strWhatUserCanDo As String) As DialogResult
The sample
solution includes an interactive demonstration of all the different parameters
you can use to call the
HandledExceptionManager.ShowDialog
method.
Conclusion
I think that wraps it up. I've used these classes on a few
different projects now with great results, and I hope they work well for you
too. My
last
article was criticized for not being detailed enough, so I tried to go into
greater depth for this one. That said, there are still a bunch of things in the
source I either didn't have space to cover, or that didn't neatly fit within the
scope of exception handling. There are many more details and comments in the
source code provided at the top of the article, so check it out. And please
don't hesitate to provide feedback, good or bad!
History
- Tuesday, June 22, 2004 - Published.
- Monday, August 16, 2004 - Updated demo code
- Added exception class to the titles of the exception emails; instead of
just the generic "Unhandled Exception", you get "Unhandled Exception -
System.IO.FileNotFoundException" and so forth
- Removed my email as the default destination for the demo emails (doh!)
- Converted labels to RichTextBoxes. We get
clickable http: and mailto: support, and scroll bars when the text exceeds
the available size. (Thanks elp?)
- Added support for outgoing SMTP authentication (thanks Chuck Peper)
- Removed embedded icon resources in favor of standard .NET icons available in the Drawing namespace (thanks Chuck Peper)
- Many other small improvements