ASP.NET 2.0 Unhandled Exception Issues

by Peter A. Bromberg, Ph.D.

Peter Bromberg

In a previous article detailing some techniques for dealing with Unhandled Exceptions, I made mention of the fact that in the .NET 2.0 Framework, unlike in .NET 1.1, an unhandled exception will smartly bring the AppDomain to a screeching halt, often without providing so much as a clue about exactly what went wrong and where in your code it happened. I also provided some sample code that can help in this area. However, I didn't go very deeply into the situation where this may happen in an ASP.NET 2.0 application other than to provide some sample code for handling the Application_Error event in global.asax. In this article, I'll continue with more details on dealing with this issue in ASP.NET 2.0.



Just to quickly review this behavior: In .NET 1.1, an unhandled exception would terminate the process only if the exception occurred on the main application thread. With anything else, the CLR would eat the exception and allow your app to keep going. As I mentioned in the previous piece, this is generally very evil, because all kinds of nasty managed and unmanaged atrocities could occur until your application simply wasn't actually accomplishing anything -- and you wouldn't be able to get a clue as to why this happened. An unhandled exception in a running ASP.NET 2.0 application will usually terminate the W3WP.exe process, and leave you with a very cryptic EventLog entry something like this:

EventType clr20r3, P1 w3wp.exe, P2 6.0.3790.1830, P3 42435be1, P4 app_web_ncsnb2-n, P5 0.0.0.0, P6 440a4082, P7 5, P8 1, P9 system.nullreferenceexception, P10 NIL.

With IIS 6, w3wp.exe abruptly terminates. I have seen two event log entries; The first is a warning entry accompanied by some detail, including a stack trace, but only if pdb files are available, which of course is rarely the case in a release-built, in-production application. The second entry is our cryptic friend above.

If you are doing any kind background threading work, or using the Threadpool, or any type of "Fire and Forget" pattern, or even if you just get a BTH ("Bad Thing Happened") where there isn't adequate try/catch/finally semantics in place, this can happen to you. Sure, a new worker process will get spun into place, but most developers would agree that this kind of abrupt termination of our worker process isn't exactly in the category of "good application behavior".

There is a setting you can put in machine.config to go "back to the Future" and have ASP.NET 1.1 behavior, but you really DO NOT want to do that. What you really want is to have a way to get enough information about your unhandled exception(s) so that you can go back and FIX IT. That's "good developer behavior"!

There is a KB that covers this here, and although it isn't one of the best-written KB's I've seen, it does provide us with a way to get more information by hooking the UnhandledException event with an HttpModule designed to provide us with more information about what went wrong. While the KB provides sample code and instructs you to put the built HttpModule assembly in "C:\Program Files\Microsoft Visual Studio 8\VC" (which is really wrong, that's the Visual C++ folder, and there's no good reason to put it there anyway -- it's unlikely to even exist on your web server), it also provides info on how to GAC it and NGEN it and strong name it, all in the cryptic MSDN style of doing everything on the command line (I guess assuming that enough people who don't own Visual Studio never heard of SharpDevelop or other free tools).

At any rate, it is certainly not necessary to strong name and GAC and NGEN it; there is no reason why it can't just be placed in the bin folder of a specific app, provided that the HttpModule web.config entries are in place.

I've built and tested a VS.NET 2005 version of this and have included the solution which also contains a simple web test IIS application to test it out. Let's look at some sample code on how we can generate an unhandled exception by the use of a "fire and forget" call to a ThreadPool thread:

protected void Button1_Click(object sender, EventArgs e)
{

ThreadPool.QueueUserWorkItem( new WaitCallback(TestMe), null );
}

protected void TestMe(object state)
{
string s = state.ToString();
}

What the above code does in response to your button click is to Queue a new WorkItem onto the default Threadpool with a WaitCallback on the TestMe method, passing in a null state object. (a WaitCallback defines a callback method to be executed by a thread pool thread). The TestMe method will always throw a NullReference exception, but since we've called it on a background (not the main) thread, which thread is not in the request processing context, the exception will not reach the standard Application_Error event handler. This would be true for QueueUserWorkItem or if using a separate thread directly. So your worker process in ASP.NET 2.0 will go right into the Black Hole and all you'll get is the blue cryptic EventType clr20r3,.... EventLog entry as above.

However, with the HttpModule hooked up, you can get this:

UnhandledException logged by UnhandledExceptionModule.dll:

appId=/LM/W3SVC/1/Root/TestWeb

type=System.NullReferenceException

message=Object reference not set to an instance of an object.

stack=
at _Default.TestMe(Object state) in c:\CSHARPBIN2\UnhandledExceptionModule\TestWeb\Default.aspx.cs:line 28
at System.Threading._ThreadPoolWaitCallback.WaitCallback_Context(Object state)
at System.Threading.ExecutionContext.runTryCode(Object userData)
at System.Runtime.CompilerServices.RuntimeHelpers.ExecuteCodeWithGuaranteedCleanup(TryCode code, CleanupCode backoutCode, Object userData)
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback(Object state)

As our EventLog entry shows, we now have sufficient information to be able to go back to our source code and figure out a way "not to have" our Unhandled Exception and code more defensively.

Here's the source for the HttpModule:

using System;

using System.Diagnostics;

using System.Globalization;

using System.IO;

using System.Runtime.InteropServices;

using System.Text;

using System.Threading;

using System.Web;

 

namespace WebMonitor

{

    public class UnhandledExceptionModule : IHttpModule

    {

 

        static int _unhandledExceptionCount = 0;

 

        static string _sourceName = null;

        static object _initLock = new object();

        static bool _initialized = false;

 

        public void Init(HttpApplication app)

        {

 

            // Do this one time for each AppDomain.

            if (!_initialized)

            {

                lock (_initLock)

                {

                    if (!_initialized)

                    {

 

                        string webenginePath = Path.Combine(RuntimeEnvironment.GetRuntimeDirectory(), "webengine.dll");

 

                        if (!File.Exists(webenginePath))

                        {

                            throw new Exception(String.Format(CultureInfo.InvariantCulture,

                                "Failed to locate webengine.dll at '{0}'.  This module requires .NET Framework 2.0.",

                                webenginePath));

                        }

 

                        FileVersionInfo ver = FileVersionInfo.GetVersionInfo(webenginePath);

                        _sourceName = string.Format(CultureInfo.InvariantCulture, "ASP.NET {0}.{1}.{2}.0",

                            ver.FileMajorPart, ver.FileMinorPart, ver.FileBuildPart);

 

                        if (!EventLog.SourceExists(_sourceName))

                        {

                            throw new Exception(String.Format(CultureInfo.InvariantCulture,

                                "There is no EventLog source named '{0}'. This module requires .NET Framework 2.0.",

                                _sourceName));

                        }

 

                        AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(OnUnhandledException);

 

                        _initialized = true;

                    }

                }

            }

        }

 

        public void Dispose()

        {

        }

 

        void OnUnhandledException(object o, UnhandledExceptionEventArgs e)

        {

            // Let this occur one time for each AppDomain.

            if (Interlocked.Exchange(ref _unhandledExceptionCount, 1) != 0)

                return;

 

            StringBuilder message = new StringBuilder("\r\n\r\nUnhandledException logged by UnhandledExceptionModule.dll:\r\n\r\nappId=");

 

            string appId = (string)AppDomain.CurrentDomain.GetData(".appId");

            if (appId != null)

            {

                message.Append(appId);

            }

 

            Exception currentException = null;

            for (currentException = (Exception)e.ExceptionObject; currentException != null; currentException = currentException.InnerException)

            {

                message.AppendFormat("\r\n\r\ntype={0}\r\n\r\nmessage={1}\r\n\r\nstack=\r\n{2}\r\n\r\n",

                    currentException.GetType().FullName,

                    currentException.Message,

                    currentException.StackTrace);

            }

 

            EventLog Log = new EventLog();

            Log.Source = _sourceName;

            Log.WriteEntry(message.ToString(), EventLogEntryType.Error);

        }

 

    }

}

With this built (and you should certainly consider GAC'ing and NGENing it with the commands shown in the KB article) all we need is the HttpModule entry in web.config to use it:

<httpModules>
<add name="UnhandledExceptionModule" type="WebMonitor.UnhandledExceptionModule, UnhandledExceptionModule, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9ccd488d04236749, processorArchitecture=MSIL"/>
</httpModules>

You need to get the strong name info out of the GAC, which can be done by using the command line statement:

gacutil /l UnhandledExceptionModule

at a VS.NET 2005 command prompt in the output folder of your HttpModule.

I hope this addition to your toolkit never fires, but you know Murphy's Law! The Solution download includes all code, the web test app, and a strong name key all built in.

 

Download the Visual Studio 2005 Solution that accompanies this article


Peter Bromberg is a C# MVP, MCP, and .NET consultant who has worked in the banking and financial industry for 20 years. He has architected and developed web - based corporate distributed application solutions since 1995, and focuses exclusively on the .NET Platform.
Article Discussion: