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

ASP.NET Application Error Handling

0.00/5 (No votes)
5 Jun 2013 1  
Application error handling in ASP.NET

Introduction

When an unhandled exception occurs in my application, I want my application to give the user a "graceful" response. Regardless of the error, I do not want the user to see an unfriendly technical error messages generated by IIS or ASP.NET. At the same time, I want to receive email notification for every unhandled exception.

This article describes a simple and comprehensive solution to this problem.

The Problem

When I have no error handling configured for my application, my users might see any one of three different error pages, depending on the type of error.

If a user requests a static resource that does not exist (for example, an HTML or JPG file), then the user sees the default HTTP error message generated by IIS:

If a user requests a dynamic resource that does not exist (for example, an ASPX file), then the user sees the default server error message generated by ASP.NET for HTTP 404 errors:

If an unhandled exception occurs in the application, then the user sees the default server error message generated by ASP.NET for HTTP 500 errors:

ASP.NET web application developers sometimes call these the "Blue Screen of Death" (BSOD) and the "Yellow Screen of Death" (YSOD).

The Solution

When a user sees an error message in my application, I want the error message to match the layout and style of my application, and I want every error message to follow the same layout and style.

Step 1: Integrated Pipeline Mode

As a first step, I set my application to use an application pool that is configured for Integrated managed pipeline mode.

Microsoft Internet Information System (IIS) version 6.0 (and previous versions) integrates ASP.NET as an ISAPI extension, alongside its own processing model for HTTP requests. In effect, this gives two separate server pipelines: one for native components and one for managed components. Managed components execute entirely within the ASP.NET ISAPI extension -- and only for requests specifically mapped to ASP.NET. IIS version 7.0 and above integrates these two pipelines so that services provided by both native and managed modules apply to all requests, regardless of the HTTP handler.

For more information on Integrated Pipeline mode, refer to the following Microsoft article:

Step 2: Application Configuration Settings

Next, I add error pages to my application for 404 and 500 error codes, and I update the application configuration file (web.config) with settings that instruct IIS to use my custom pages for these error codes.

<system.webServer>
  <httpErrors errorMode="Custom" existingResponse="Replace">
    <remove statusCode="404"/>
    <remove statusCode="500"/>
    <error statusCode="404" responseMode="ExecuteURL"
    path="/Pages/Public/Error404.aspx"/>
    <error statusCode="500" responseMode="ExecuteURL"
    path="/Pages/Public/Error500.aspx"/>
  </httpErrors>
</system.webServer>

Now, when a user requests a static resource that does not exist, the user sees the error message generated by my custom page:

Similarly, if a user requests a dynamic resource that does not exist, then the user sees the error message generated by my custom page:

And finally, if an unhandled exception occurs in the application, then the user sees the error message generated by my custom page:

Step 3: Exception Details

The first part of my problem is now solved: the error messages seen by my users are rendered inside a page that is consistent with the layout and style of my application, and the error messages themselves are consistent regardless of the underlying cause of the unhandled exception.

However, in this configuration, when an unhandled exception occurs and IIS executes Error500.aspx, the code behind this page has no details for the exception itself. When an unhandled exception occurs, I need a crash report saved to the file system on the server and sent to me by email. The crash report needs to include the exception details and a stack trace so that I can find and fix the cause of the error.

Some articles suggest that I can identify the unhandled exception using Server.GetLastError, but I get a null value whenever I attempt this.

Exception ex = HttpContext.Current.Server.GetLastError(); // <-- Returns null in Error500.aspx

Note: I have been unable to find a clear explanation for this in Microsoft's documentation. (Please drop me a note if you can direct me to the documentation that explains this behaviour.)

I can solve this by adding an HTTP module with an event handler for application errors. For example, after I add this code to Global.asax, my custom error page can access the current cache for the exception details.

protected void Application_Error(object sender, EventArgs e)
{
    Exception ex = HttpContext.Current.Server.GetLastError();
    CrashReport report = CrashReporter.CreateReport(ex, null);
    HttpContext.Current.Cache[Settings.Names.CrashReport] = report;
}

It is important to note that if I add code at the end of my event handler to invoke Server.ClearError(), my custom error message is NOT displayed to the user (and neither is the default server error message). In fact, if I invoke ClearError() here, then the error message becomes a blank page, with no HTML in the output rendered to the browser. Remember, the purpose of the event handler in this configuration is to store exception details in the current cache (or in the session state) so that it is accessible to the Error500.aspx page. The purpose is NOT to handle the exception itself, and this is the reason the error is not cleared here.

Note: Referring to my earlier point, if I have not cleared the error here, because it is required in order to ensure that my custom error page is executed, then it is not obvious why the call to Server.GetLastError() in the custom error page returns a null value. If you have an explanation for this, then please post a comment.

Improving the Solution

My solution needs to write a crash report to the file system (so we have a permanent record of the event) and it needs to send an email notification (so we are immediately alerted to the event). At any given time, my company is actively developing dozens of applications for various customers, so a reusable solution is important.

Code added to Global.asax is not easily reused across multiple applications, so I created an HTTP module (i.e., a class that inherits from System.Web.IHttpModule), which I can subsequently add to a library and then reference from many different applications.

In order for this solution to work, I add the following settings to the system.webServer element in my web application configuration file (Web.config):

<modules>
    <add name="ApplicationErrorModule" type="Demo.Classes.ApplicationErrorModule" />
</modules>

The code to wireup handling for an application error is simple:

public void Init(HttpApplication application)
{
    application.Error += Application_Error;
}

private void Application_Error(Object sender, EventArgs e)
{
    if (!Settings.Enabled)
        return;

    Exception ex = HttpContext.Current.Server.GetLastError();

    if (UnhandledExceptionOccurred != null)
        UnhandledExceptionOccurred(ex);

    ExceptionOccurred(ex);
}

The code to process the exception itself is basically the same as the code I originally added to the global application event handler, but here I also add code to save the crash report to the file system, and to send a copy to me by email.

private static void ExceptionOccurred(Exception ex)
{
    // If the current request is itself an error page 
    // then we need to allow the exception to pass through.

    HttpRequest request = HttpContext.Current.Request;
    if (Regex.IsMatch(request.Url.AbsolutePath, ErrorPagePattern))
        return;

    // Otherwise, we should handle the exception here

    HttpResponse response = HttpContext.Current.Response;
    CrashReport report = new CrashReport(ex, null);

    // Save the crash report in the current cache 
    // so it is accessible to my custom error pages

    if (HttpContext.Current.Cache != null)
        HttpContext.Current.Cache[Settings.Names.CrashReport] = report;

    // Save the crash report on the file system

    String path = SaveCrashReport(report, request, null);

    // Send the crash report to the programmers

    SendEmail(report, path);

    // Write the crash report to the browser 
    // if there is no replacement defined for the HTTP response

    if (!ReplaceResponse)
    {
        HttpContext.Current.Server.ClearError();

        try
        {
            response.Clear();
            response.StatusCode = 500;
            response.StatusDescription = "Server Error";
            response.TrySkipIisCustomErrors = true;
            response.Write(report.Body);
            response.End();
        }
        catch { }
    }
}

The last part of this function is especially important. If, for some reason, I forget to include the httpErrors section in my webserver configuration element, then I want the body of my crash report rendered to the browser and not the default Yellow Screen of Death (YSOD) that ASP.NET shows when a server error occurs.

Points of Interest

There are many good articles on the topic of ASP.NET application error handling, and there are many good products that are helpful in the development of solutions. Here are just a few references for more information:

History

June 1, 2013: When the BaseErrorPage is loaded on a PostBack request, the response is assigned an HTTP status code of 200 (OK). This enables the "Submit Quick Error Report" feature on the error page.

June 5, 2013: Modified the code to save the crash report for an unhandled exception using a session-safe key. This corrects for the scenario in which multiple concurrent users encounter different exceptions at the same time.

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