RSS

Monthly Archives: May 2013

Handling unhandled Task exceptions in ASP.NET 4

This blog post isn’t intended to explain how to use IsFaulted or any other method for handling Task exceptions in a specific block of code, but how to provide a general solution to handling unhandled Task exceptions, specifically in ASP.NET. The problem dealt with in this post is a situation that a developer didn’t handle an exception in the code for whatever reason, and the process terminates as a result.

I came across a problematic situation – in a production environment, an ASP.NET IIS process seems to be terminating and initializing constantly. After some research, it turned out that there’s a certain Task that was raising exceptions, but the exceptions were not handled. It seems like the Application_Error handler was not raised for these exceptions. It seems that in .NET 4, unhandled Task exceptions terminate the running process. Fortunately, Microsoft changed this behavior in .NET 4.5 and by default the process is no longer terminated. It is possible to change that behavior to the previous policy by tweaking the web.config, although I find it hard to think why one would want to do that, except maybe in a development environment in order to be aware that such unhandled exceptions exist.

Back to .NET 4, we still have to prevent termination of the process. The first issue was to find how to catch unhandled Task exceptions, as it became clear that Application_Error wasn’t handling these exceptions. I googled for a solution. Turns out that there’s an UnobservedTaskException event that is designated for this exact purpose. It’s a part of the TaskScheduler class. Whenever an unhandled Task exception is thrown, it may be handled by event handlers wired to this event. The code block below is an example how this event can be put into use in a global.asax file.

    void Application_Start(object sender, EventArgs e)
    {
        // Code that runs on application startup
        System.Threading.Tasks.TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
    }

    void TaskScheduler_UnobservedTaskException(object sender, System.Threading.Tasks.UnobservedTaskExceptionEventArgs e)
    {
        e.SetObserved();
    }

As you can see, when you call the SetObserved() method, this marks the exception as “handled”, and the process will not terminate.

Note, that the exception is basically thrown when the Task is GC-ed. This means that as long as there are references to the Task instance it will not be garbage collected and an exception will not be thrown.

Depending on the TaskCreationOptions, the raised events and event handling may vary in behavior. For example, if you have nested Tasks throwing exceptions, and the TaskCreationOptions is set to be attached to the parent, a single UnobservedTaskException event will be raised for all those exceptions, and you may handle each of these exceptions differently, if required. The incoming exceptions in such a case are not “flattened” but nested as well, and you may iterate recursively on the different exceptions in order to treat each and everyone independently. However, you may call on the Flatten() method to receive all the exceptions in the same “level” and handle them as if they were not nested.

In a test page, a button_click event handler raises three exceptions. The nested exceptions are AttachedToParent, which affects how they will be received in the UnobservedTaskException event handling. I added a GC_Click button and event handler to speed things up:

    protected void Button_Click(object sender, EventArgs e)
    {
        Task.Factory.StartNew(() =>
        {
            Task.Factory.StartNew(() =>
            {
                Task.Factory.StartNew(() =>
                {
                    throw new ApplicationException("deepest exception");
                }, TaskCreationOptions.AttachedToParent);

                throw new ApplicationException("internal exception");
            }, TaskCreationOptions.AttachedToParent);

            System.Threading.Thread.Sleep(100);
            throw new ApplicationException("exception");
        }, TaskCreationOptions.LongRunning);
    }

    protected void GC_Click(object sender, EventArgs e)
    {
        GC.Collect();
    }

You can see in the Watch window, how these exceptions are received in the event handler by default:
1

And with the Flatten() method:
2

Note: If the Task creation is set differently, for example to LongRunning, each exception will have its own event handling. In other words, multiple UnobservedTaskException event handlers will be raised.

Now comes the other part where you would probably want to Log the different exceptions. Assuming that you would want to log all the exceptions, regardless of their “relationship”, this is one way to do it, assuming a Log method exists:

    void TaskScheduler_UnobservedTaskException(object sender, System.Threading.Tasks.UnobservedTaskExceptionEventArgs e)
    {
        e.SetObserved();
        e.Exception.Flatten().Handle(ex =>
        {
            try
            {
                Log(ex);
                return true;
            }
            catch { return true; }
        });
    }

The Handle method allows iteration over the different exceptions. Each exception is logged and a true if returned to indicate that it was handled. The try-catch there is designated to ensure that the logging procedure itself won’t raise exceptions (obviously, you may choose not to do that). You should also take into account that if you would like to implement some kind of particular logic that determines whether the exception should be handled or not, you may choose to return false for exceptions not handled. According to MSDN, this would throw another AggregateException method for all the exceptions not handled.

Next step: apply the exception handling to an existing application in production.
If this code is to be incorporated during development, simple change to global.asax would do. But this had to be incorporated in a running production environment. If this is a web site, then it would still be possible to patch the global.asax. Just have to schedule a downtime as ASP.NET will detect a change to global.asax and the application will be restarted. The biggest advantage is of course the ability to change the global.asax in a production environment as ASP.NET will automatically compile it.

But for a web application this is different. As the global.asax.cs is already compiled into an assembly in a production environment, there are basically two options: either compile the solution and deploy an upgrade, or write an http module. Writing a module probably makes more sense as you don’t have to compile and deploy existing assemblies. You just have to add an HttpModule to an existing web app.

Here’s an example for such a module:

    public class TaskExceptionHandlingModule : IHttpModule
    {
        public void Dispose() { }

        public void Init(HttpApplication context)
        {
            System.Threading.Tasks.TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
        }

        void TaskScheduler_UnobservedTaskException(object sender, System.Threading.Tasks.UnobservedTaskExceptionEventArgs e)
        {
            e.SetObserved();
            e.Exception.Flatten().Handle(ex =>
            {
                try
                {
                    Log(ex);
                    return true;
                }
                catch { return true; }
            });
        }

        private void Log(Exception ex)
        {
            // TODO: log the exception
        }
    }

All that is left to do is place the compiled http module in the bin folder and add a reference to it in the web.config. Note: The location within the web.config may vary depending on the version of the IIS used.

<httpModules>
<add name="TaskExceptionHandlingModule" type="TaskExceptionHandlingModule.TaskExceptionHandlingModule, TaskExceptionHandlingModule"/>
</httpModules>

Notice that changing the web.config and placing the module in the bin folder of the web application will cause it to restart, so this has to be done at a coordinated downtime.

Assuming the module was placed in the correct location and that the web.config was properly configured, the process is no longer expected to terminate due to unhandled task exceptions, and those exceptions should now be logged.

 

 
Leave a comment

Posted by on 26/05/2013 in Software Development

 

Tags: ,