ASP.NET MVC Tutorial: Handling Errors and Exceptions
Common practices for handling errors and trapping exceptions
March 4, 2012
RELATED: "Using Data Annotations for ASP.NET MVC 3 Input Validation" and "Exploring the Razor Syntax in ASP.NET MVC 3"
In ASP.NET MVC, error handling can be split in two parts: handling errors and exceptions that occur within the code and handling exceptions at the framework level. You can easily deal with the first type of exceptions; however, you have to intervene in various places and use different tools to neutralize the impact of route exceptions and HTTP errors. In the end, you gain total control over runtime exceptions by writing error handlers within controllers and at least a global exception handler in global.asax. Let's find out the details and explore common practices for handling exceptions in ASP.NET MVC.
Catching Exceptions in Controllers
In controllers you write plain code, and in plain code you typically catch exceptions by using try/catch blocks. This approach gives you the most flexibility but at the cost of adding some noise to the code. Having a bunch of try/catch blocks scattered through a single method, though effective, makes reading the code a bit more difficult. The point here is not to question the importance of exception handling but simply to consider whether there's a better way of achieving the same results using easier-to-read code.
Conveniently in this regard, Microsoft offers us the OnException overridable method and the HandleError filter attribute. Both methods -- and one method doesn't exclude the other -- allow us to trap any exceptions raised around the controller code without having to write any explicit try/catch blocks. In particular, the OnException method that's defined on the base controller class behaves like a predefined global exception handler that simply wraps up any controller method you may have.
To explain this in more detail, in ASP.NET MVC the execution of each controller method is governed by an action invoker. This is a system component responsible for executing a controller method, capturing any response it may generate (typically an ActionResult object), and using that response to generate a view or package a response for the client browser.
An interesting aspect of the default action invoker is that it always executes controller methods within a try/catch block. In this way, if your controller code fails in handling an exception, that exception will never bubble up the stack toward the top, and the classic ASP.NET yellow error page will not be displayed. Any exceptions that go unhandled in your controller code are sure to be caught by action invoker handlers. Figure 1 shows the code that the default action invoker uses internally to run your controller methods.
try{ // Invoke the action method here ...}catch(ThreadAbortException){ throw;}catch(Exception exception){ // Prepare the context for the current action var filterContext = PrepareActionExecutedContext( ..., exception); // Go through the list of registered HandleError action filters // and give them a chance to recover ... // Re-throw if not completely handled if (!filterContext.ExceptionHandled) { throw; }}
As you can see, only ThreadAbort exceptions are thrown unchanged; every other exception is captured and run by the registered list of HandleError filter attributes. The controller itself is seen as a registered error-handler filter as long as it overrides the OnException method. At the end of the loop, if the exception has not been marked as handled, the originally caught exception is then re-thrown. As mentioned, a controller is added to the list if it contains code like the following example -- namely if it overrides the OnException method defined on the controller base class.
protected override void OnException(ExceptionContext filterContext){ ...}
Having an OnException method in a controller class ensures that no exceptions will ever go unhandled except those you deliberately leave out of your handler. The OnException method receives a parameter of type ExceptionContext. This type comes with a Result property of type ActionResult, which refers to the next view or action result. Unless the code in OnException sets a result, the user won't see any error page, only a blank screen. Figure 2 shows a possible implementation of the OnException method.
protected override void OnException(ExceptionContext filterContext){ // Let other exceptions just go unhandled if (filterContext.Exception is InvalidOperationException) { // Switch to an error view ... }}
Switching to an error view doesn't mean a redirection to a different page or URL. It simply indicates an on-the-fly change of the view template that's used to prepare the response for the browser. The code in Figure 3 shows what you need to do to switch to a different view from within OnException.
var model = new HandleErrorInfo(context.Exception, controllerName, actionName);var result = new ViewResult { ViewName = view, MasterName = master, ViewData = new ViewDataDictionary(model), TempData = context.Controller.TempData };context.Result = result; // Configure the response object context.ExceptionHandled = true;context.HttpContext.Response.Clear();context.HttpContext.Response.StatusCode = 500;context.HttpContext.Response.TrySkipIisCustomErrors = true;
It is worth noting that you don't need this code if in your application it's acceptable to display to users a common page for any exception caught. In this case, the default view is a view named error. You can find an example implementation of such a view in any ASP.NET MVC project generated via the Visual Studio ASP.NET MVC project template.
The HandleError Attribute
As an alternative to overriding the OnException method, you can decorate the class (or just individual methods) with the HandleError attribute. The attribute is not purely declarative as it contains some logic that allows developers to indicate what to do when a given exception occurs. The HandleError attribute traps any exceptions or only those that you indicate through properties. Here's an example:
[HandleError(ExceptionType=typeof(NullReferenceException), View="SyntaxError")]
Each method can have multiple occurrences of the attribute, one for each exception you're interested in handling. The View property indicates the name of the view to display after the exception is trapped. By default, HandleError switches to the default error view. You should note that for HandleError to produce any visible results while in debug mode you need to enable custom errors at the application level. To do so, enable this setting in your web.config file:
If you leave on the default settings for the section of the configuration file, only remote users will get the selected error page. Developers who are doing local debugging will instead receive the classic error page with detailed information about the stack trace.
In ASP.NET MVC 3, the HandleError attribute -- just like any other action filter attribute -- can be registered as a global filter, meaning that it will be automatically applied to any method in any controller class. This is precisely what the standard ASP.NET MVC 3 project template in Visual Studio 2010 hard-codes in global.asax. In light of this, any exceptions in any controller methods are automatically trapped and redirected to the default error view.
Global Error Handling
Dealing with errors at the controller level doesn't ensure that you intercept all possible exceptions that may be raised around your application. Exceptions can occur because of failures in the model-binding layer or resulting from picking the wrong route or the right route but with wrong parameters. To make sure you can handle any possible exceptions, you might want to create a global error handler at the application level that catches all unhandled exceptions and routes them to the specified error view.
Since the very first version of the ASP.NET runtime, the HttpApplication object -- the object behind global.asax -- has featured an Error event. The event is raised whenever an unhandled exception reaches the outermost shell of code in the application. Here's how to write such a handler:
void Application_Error(Object sender, EventArgs e){ ...}
You could do something useful in this event handler, such as sending an email to the site administrator or writing to the Windows event log details about the detected exception. When using the global error handler, you might also want to use a landing page to redirect users immediately after the application has performed the error-handling code. To reach the landing (error) page, you need to use a classic ASP.NET redirect. At this time, in fact, you are outside of the ASP.NET MVC default action invoker and have no chance to simply indicate a different view; a standard HTTP 302 redirect is the only way to go.
A centralized error handler is also good at catching exceptions that originate outside the controller, such as exceptions that occur because of incorrect parameters. If you declare a controller method with, say, one integer argument, and the current binder can't match any posted value to it, you get an exception. These exceptions cannot be trapped other than by using a global handler in global.asax.
Route Exceptions
Your application might also be throwing exceptions because the URL of the incoming request doesn't match any of the known routes. This can happen because an invalid URL pattern is provided or because of some invalid route parameters that violate a route constraint. In this case, your users get an HTTP 404 error. A page-not-found HTTP exception is, however, something you might want to avoid for a number of reasons, but primarily to be kind to your end users.
Instead of a system error page, you might want to define custom routes in ASP.NET MVC for common HTTP codes such as 404 and 403, as in the following example:
...
Whenever the user types or follows an invalid URL, the user is redirected to another view where ideally some useful information is provided. If you use a different landing view for different HTTP status codes, then you potentially disclose to hackers information that they could use to plan further attacks. But always returning the same response for any incorrect URL attempted reveals no significantly usable data to potential hackers. Because the system's reaction is always the same, there's not much that hackers can learn about your system. So you explicitly set the defaultRedirect attribute of the section to a given and fixed URL and ensure that no per-status codes are ever set.
More often than not, however, route exceptions refer to some missing content. For this reason, it is always a good idea to append a catch-all route to the list of registered routes. This indicates that if no previous (and more specific) routes made the cut, the user is simply trying to reach your application using an invalid URL. You catch that request and process it to display a user-friendly error view.
A classic catch-all route might look like the following example:
routes.MapRoute( "Catchall", "{*anything}", new { controller = "Error", action = "Missing" } );
According to this code, the request will be handed to the Missing method on the Error controller. Here you just prepare a user-friendly error view and serve that to users.
Effective Error Handling
Error-handling code is always quite boring to write. Furthermore, standard try/catch blocks will make your code harder to read. However, these are certainly not acceptable reasons to skip over exception handling and let the system deal with any exceptions that go unhandled. Along with evergreen techniques like try/catch blocks, ASP.NET MVC provides some facilities aimed at making the error-handling code explicit in your source files only when strictly needed. The HandleError attribute and the OnException virtual method on controller classes offer a great contribution to handling errors while keeping your code base as clean and pure as possible.
About the Author
You May Also Like