Catching Unhandled Exceptions in .NET

Posted on

At Resolver, we've been looking at better ways of dealing with unhandled exceptions that occur during test runs. Apart from the need to log that a problem occurred it is important that the dialog boxes that Windows generates don't block the test run (ideally they wouldn't appear at all). We had a hack in place to deal with these dialogs that I won't go into here. Let's just say we've been finding our hack inadequate.

In the .NET world there's 2 APIs that your program can use to be notified about unhandled exceptions. Each covers exceptions that happen in different parts of your code. In order to be comprehensive about catching unhandled exceptions you really need to use both APIs.

The first is the Application.ThreadException event. Despite what the name seems to indicate, this catches unhandled errors that occur during normal execution of Windows Forms applications. That is, in event handlers once Application.Run has been called.

Here's a quick example of how to use it from IronPython.

import clr
clr.AddReference('System')
clr.AddReference('System.Windows.Forms')
from System.Threading import ThreadExceptionEventHandler
from System.Windows.Forms import Application, UnhandledExceptionMode, Form, Button

def handler(sender, event):
    print 'Oh no!'
    print event.Exception
    Application.Exit()

Application.ThreadException += ThreadExceptionEventHandler(handler)
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException)

def die(_, __):
    raise ValueError('foo')
form = Form()
button = Button(Text='Click me')
button.Click += die
form.Controls.Add(button)
Application.Run(form)

The second API is AppDomain.CurrentDomain.UnhandledException. This event is triggered when unhandled exceptions occur in threads other than the main thread. There is one caveat with using this event: Windows will still pop up its own dialog box even if you've installed your own handler! This is somewhat frustrating when you want to run unattended test runs as we do at Resolver. Our builds would block until someone comes along and closes the dialog.

It seems that many others face the same problem and there's no good solutions reported online. The only option I could find was to write some C++/C# that bypasses the .NET handler by using the SetUnhandledExceptionFilter Win32 call. The CLR uses the same underlying Win32 API to do its unhandled exception handling so by installing your own handler here you can prevent the .NET handler from firing and prevent the dialog box from appearing. The problem with this approach is that you probably don't have access to a useful CLR traceback.

This morning it occurred to me that perhaps if the process terminates during the unhandled exception handler then the CLR won't have the opportunity to show its own dialog. We had tried Application.Exit() without success (the dialog still appears) but terminating the current process with a Kill() did the trick! Here's how the code looks...

import clr
clr.AddReference('System')
clr.AddReference('System.Drawing')

from System import AppDomain, UnhandledExceptionEventHandler
from System.Diagnostics import Process
from System.Threading import Thread, ThreadStart

def handler(sender, event):
    print 'AppDomain error!'
    print event.ExceptionObject
    Process.GetCurrentProcess().Kill()

AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionEventHandler(handler)

def die():
    raise ValueError('foo')
t = Thread(ThreadStart(die))
t.Start()
Thread.Sleep(5000)

It's ugly but it works. I'd love to know if there's a better way of dealing with this.

Hopefully this helps some people out there who have struggled with the same issue. Although these examples are in IronPython, the principles should easily translate to C# and other .NET languages.