Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

User Friendly Exception Handling

0.00/5 (No votes)
17 Aug 2004 1  
A flexible framework for user-friendly .NET exception handling, and automatically notifying developers of problems before users do

Sample Image - ExceptionHandling.gif

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.

  1. 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.
  2. 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.
  3. 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!
  4. 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.

screenshot of unhandled exception UI in a winforms app

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:

screenshot of unhandled exception UI in a console app

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.

screenshot of files created in the bin folder

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

screenshot of automatic exception email

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.

screenshot of application event log

Two final observations on Unhandled Exceptions before we close our discussion.

  1. 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.
  2. 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.

screenshot of handled exception UI

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
    1. 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
    2. Removed my email as the default destination for the demo emails (doh!)
    3. Converted labels to RichTextBoxes. We get clickable http: and mailto: support, and scroll bars when the text exceeds the available size. (Thanks elp?)
    4. Added support for outgoing SMTP authentication (thanks Chuck Peper)
    5. Removed embedded icon resources in favor of standard .NET icons available in the Drawing namespace (thanks Chuck Peper)
    6. Many other small improvements

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here