The .NET Framework 2.0 no longer allows background threads to die silently when an unhandled exception is thrown. I'm not going to dive into the details of the change, as it is already well-documented on the web (
this post describes the behavior for different application types &
Scott Allen focuses on the ASP.NET impact).
The impact is more significant for ASP.NET applicationsI think this is actually a bigger change for ASP.NET apps, because it has always had a "safety net". In console/WinForms applications, if there is an unhandled exception on the
primary thread, the application dies. Having your application die now because of an unhandled exception on a
background thread is not that big of a difference. You have an unhandled exception in your application, the application dies. Consistent.
However, in ASP.NET, exceptions that you did not handle in your own code on the
primary thread were always caught by a default handler (affectionately known as the "
yellow screen of death"). Your application did not die. You could add a handler for the HttpApplication.Error event (Application_Error in global.asax) to centralize your exception handling and logging logic. In 1.x, exceptions that occurred on a background thread would die silently. Your application did not die. Consistent. But now, in 2.0, exceptions on your primary thread still do not kill your application, and can be logged by your global handler. But exceptions on your background threads will kill your application, and will not be logged.
My approach to exception handling and how it is now more difficult to implementMy philosophy is to never catch exceptions unless there is something I can do to address the issue. I prefer not to catch all possible exceptions throughout the internals of my code, and instead rely on try...finally blocks to clean up in case of an exception, and allow the exception to bubble up to a top-level handler where it can be logged (of course I catch exceptions when there is something the code can do to resolve the issue or continue in a known state).
The new background thread exception behavior forces me to create "top level handler and logging" code for the application (global.asax) AND within every method that I run on a background thread. I end up with a lot of duplicate code, and explicit exception handling and logging that I would prefer to be hidden behind the scenes instead of cluttering up my application logic.
A proposed solutionMy goals:
- Allow unexpected exceptions on all threads to be logged by a top level exception handler
- Do not let the application die because of an exception, no matter which thread it is running on (at least not before I get a chance to log it).
- Do not clutter the application logic with repeated try..catch and logging code.
To satisfy the top level exception handler goal, I would like to make use of the existing HttpApplication.Error / Application_Error mechanism. That means I need to somehow transfer the exception that occurred on a background thread to a primary ASP.NET thread. To satisfy the "do not die" and "do not clutter" goals, I need to wrap all calls to background thread methods with a single piece of code.
I've created the
SafeWaitCallback class. It's
Call method is used in place of a
WaitCallback delegate. For example, if I want to execute the method
DoTask on a background thread, I would call it like this:
System.Threading.ThreadPool.QueueUserWorkItem(new SafeWaitCallback().Call(DoTask));
SafeWaitCallback does two things: it wraps calls to the target method in a try...catch block, and it forwards the caught exception to a custom HttpHandler. It forwards the exception by serializing it to a byte array, and then POSTing it to the handler, which will receive the bytes and deserialize back to an Exception object.
using System;
using System.Net;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization;
public class SafeWaitCallback {
public static Uri ApplicationUri;
System.Threading.WaitCallback callback;
public System.Threading.WaitCallback Call(System.Threading.WaitCallback callback) {
this.callback = callback;
return CallbackWrapper;
}
private void CallbackWrapper(object state) {
try
{
callback(state);
}
catch (Exception e)
{
byte[] exceptionData;
MemoryStream stream = new MemoryStream();
BinaryFormatter formatter = new BinaryFormatter(null, new StreamingContext(StreamingContextStates.Persistence));
formatter.Serialize(stream, e);
exceptionData = stream.ToArray();
WebClient client = new WebClient();
Uri handler = new Uri(ApplicationUri, "TransferException.axd");
try
{
client.UploadData(handler, exceptionData);
}
catch (WebException) { }
}
}
}
By forwarding the exception to the custom HttpHandler, I can re-raise the exception, and since the handler will be running on a primary ASP.NET thread (not in the background), the exception will be caught by my global handler, and logged just like any other exception.
using System;
using System.Web;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization;
public class TransferredExceptionHandler : IHttpHandler {
public bool IsReusable { get { return true; }}
public void ProcessRequest(HttpContext context) {
byte[] exceptionData = new byte[context.Request.ContentLength];
context.Request.InputStream.Read(exceptionData, 0, exceptionData.Length);
Exception transferredException;
MemoryStream stream = new MemoryStream(exceptionData);
BinaryFormatter formatter = new BinaryFormatter(null, new StreamingContext(StreamingContextStates.Persistence));
transferredException = (Exception)formatter.Deserialize(stream);
throw new Exception("[Background exception transferred - see InnerException]", transferredException);
}
}
The handler is registered in web.config with the following entry:
<httpHandlers>
<add verb="POST" path="TransferException.axd" type="TransferredExceptionHandler" />
</httpHandlers>
Let me know what you thinkI've attached the full source code in the form of a Web Site project. It includes a Default.aspx page which demonstrates the different scenarios of an unhandled exception (primary thread, background thread, "safe" background thread using my new code).
This is
not production ready code, it is still in the proof of concept stage. For one, it probably needs better exception handling within the CallbackWrapper exception handler (what if the serialization blows up?). More importantly, I'm not yet convinced this whole idea is a worthwhile approach. It meets my goals, but it feels a little hackish. I wanted to see if it was possible, and then throw it out there to get some feedback. Anyone see any major flaws in the concept? Even better, does anyone know of a more elegant approach to achieve the same goals?
Download AspNetBackgroundExceptions.zip (2.55 KB)