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.