An ASP.NET MVC Error Handler

by Dave 16-November-2009

A Simple Approach for Trapping Unhandled Errors in ASP.NET MVC

ASP.NET MVC is appealing for its simplicity of markup, real testability and overall elegance. I have jumped in and am tackling tasks that I know well in Webforms to become familiar with it. In almost all cases I am discovering that I am more productive and my application is better structured. This all has been going very smoothly...

...until I decided to hook up a top level error handler for my ASP.NET MVC web application.

I started off my error handling implementation without realizing it was going to be a little different than Webforms. In an ASP.NET Webforms website or web application, the typical scenario for error handling is now pretty well tested:

  • Configure web.config by setting <customErrors> to "On," and optionally specify pages for specific errors, such as 404 (page not found)
  • Trap errors in the Application_Error event in Global.asax
  • Implement logging and the display of the error to the user in a pretty error page

Somewhere along the way, things got cloudy, but figuring this had been done to death, I searched. I found some very good articles about error handling in ASP.NET MVC:

But there was just a little too much to do! One common approach involves setting up an error handler for each controller. While I think adding specific error handlers wherever possible is beneficial in terms of providing clarity and better user feedback, you still need a top level, fall-through handler. You at least need that to start with.

The Simple ASP.NET MVC Top Level Error Handler

With simplicity in mind, here is my fairly simple, stripped down approach to catching and displaying unhandled exceptions in ASP.NET MVC. It does what I think is the bare minimum and is pretty easy to set up.

1. Configure web.config to turn on the error handler:

        <customErrors mode="On" defaultRedirect="/Error/HttpError">
          <error statusCode="404" redirect="/Error/Http404" />
        </customErrors>		
    

2. In Global.asax.cs, cache the exception in such a way that it is preserved and uniquely associated with the correct connection. I use Request.UserHostAddress for the key to store and retrieve this.

        protected void Application_Error(object sender, EventArgs e)
        {
            Exception ex = Server.GetLastError();

            Application[HttpContext.Current.Request.UserHostAddress.ToString()] = ex;
        }
    

3. In the /Controllers folder, create an ErrorController that implements a generic error action, HttpError, and an action for 404 (page not found) errors, Http404:

    public class ErrorController : Controller
    {
	    public ActionResult HttpError()
	    {
	        Exception ex = null;

	        try
	        {
	            ex = (Exception)HttpContext.Application[Request.UserHostAddress.ToString()];
	        }
	        catch
	        {
	        }

	        if (ex != null)
	        {
	            ViewData["Description"] = ex.Message;
	        }
	        else
	        {
	            ViewData["Description"] = "An error occurred.";
	        }

	        ViewData["Title"] = "Oops. We're sorry. An error occurred and we're on the case.";

	        return View("Error");
	    }

	    public ActionResult Http404()
	    {
	        ViewData["Title"] = "The page you requested was not found";

	        return View("Error");
	    }

		// (optional) Redirect to home when /Error is navigated to directly
	    public ActionResult Index()
	    {            
	        return RedirectToAction("Index", "Home");
	    }
    }
    

I implement the Index action here to redirect to the site's home page. This is so that any explicit navigation to /Error does not simply trigger the error handler. This is sort of minor and may be unnecessary, especially given that someone can just enter a bad URL and trigger the error handler at will. This, in fact, can usually be done on any site. I just figured that specific route needed a minor bit of protection, but whatever. I would be curious if anyone else goes out of their way to handle this.

4. Implement a view, /Error/Error.aspx, that displays some very basic information that the controller can modify in the form of Title and Description data:

        <%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>

        <asp:Content ID="Content3" ContentPlaceHolderID="TitleContent" runat="server">
        Error
        </asp:Content>

        <asp:Content ID="Content4" ContentPlaceHolderID="MainContent" runat="server">

        <h2><%= Html.Encode(ViewData["Title"]) %></h2>
        <p><%= Html.Encode(ViewData["Description"])%></p>

        <div>
        Return to the <%= Html.ActionLink("home page", "Index", "Home") %>.
        </div>
        </asp:Content>
    

This entire example shows how to catch any unhandled exception, cache it and route it to /Error/HttpError, and 404 errors to /Error/Http404.

Even though I cache the exception in Application_Error and use it in the ErrorController if it exists, the second step really is optional, though I would consider certain uses of this exception data standard practice. For example, this is necessary if you want to use this data from code outside of Global.asax, because once control exits Application_Error the exception is lost. In the ErrorController, I retrieve the exception from the Application object if it exists and display it. If you simply want to display a friendly error page without any exception details, you may not care about this. If you're using something like log4net, you'll probably want to log the cached exception. If you're using something like the excellent ELMAH, you don't need to do this since it will automagically be captured.

My spell checker thinks "automagically" is a word. So it is.

Testing the Error Handler

In my projects I typically set up a page to test the error handler. Accessing this page allows me to trigger an unhandled exception which will, if error handling is configured correctly, bring up the error page. To set this up in ASP.NET MVC:

1. Create two Test actions in the ErrorController, the first to display the error test page and the second to trigger the error when the test error button is clicked and posted to the controller:

        public ActionResult Test()
        {
            return View();
        }

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Test(string confirmButton)
        {
            throw new ApplicationException("Error handler test");
        }
    

2. Create a test error view, /Error/Test.aspx, to trigger an unhandled error:

        <%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>

        <asp:Content ID="Content5" ContentPlaceHolderID="TitleContent" runat="server">
	        Test
        </asp:Content>

        <asp:Content ID="Content6" ContentPlaceHolderID="MainContent" runat="server">

	        <h2>Test the Error Handler</h2>

	        <% using (Html.BeginForm())
	           { %>    
	            <input name="confirmButton" type="submit" value="Click here to test the error handler..." />    
	        <% } %>

        </asp:Content>
    

This allows you to navigate to /Error/Test and click the button to test the error handler. This calls the Test action in the ErrorController which throws an ApplicationException. If the error handling is set up properly, you are automatically redirected to /Error/Error.aspx.

Enhancements

All of the stuff on you find on www.404errorpages.com, especially in the examples www.404errorpages.com/examples/ section, can be done to the error page to give your users better feedback.

Next Steps

This article outlines a solid and simple approach to trapping and displaying unhandled errors in an ASP.NET MVC web application. Next I am going to hook up logging and notification, using something great like log4net or ELMAH.