Exception Handling in ASP.NET MVC
Learn about the HandleError filter and discuss about the different exception handling mechanisms that ill fit to an MVC application.
Index
- Introduction
- HandleErrorAttribute
- Limitations of HandleError
- HandleError vs Application_Error
- Extending HandleError
- Returning views from Application_Error
- ELMAH
- Summary
Introduction
Exception handling is a serious matter in any application, whether it's web or desktop. Implementing a proper exception handling is important in any application. In most cases once we catch the exception we have to log the exception details to database or text file and show a friendly message to the user.
In ASP.NET applications, error handling is done mostly in two ways: at local level using try-catch blocks and at global level using application events. ASP.NET MVC comes with some built-in support for exception handling through exception filters. The
HandleError
is the default built-in exception filter. Unfortunately, theHandleError
filter not gives a complete answer to the exception handling problem and that makes us to still rely on the Application_Error
event.
In this article, we will learn about the
HandleError
filter and discuss about the different exception handling mechanisms that will fit to an MVC application.HandleErrorAttribute
Exception filters
The exception filters are attributes that can be applied over an action or a controller or even at a global level. When you apply the filter at the global level then it will handle the exceptions raised by all the actions of all the controllers. The exception filters not only catches the exceptions that are raised by the actions but also the ones that are raised by the action filters that are applied over that action.
All the exception filters implements the
IExceptionFilter
interface. The Listing 1. shows the definition of this interface. The IExceptionFilter
contains a single method called OnException
which will be called whenever an exception occurs. The ExceptionContext
parameter which derives from ControllerContext
provides access to controller, route data and HttpContext
.
Hide Copy Code
public interface IExceptionFilter
{
void OnException(ExceptionContext filterContext);
}
Listing 1. IExceptionFilter definition
The
HandleErrorAttribute
is the default implementation of the IExceptionFilter
. When you create a MVC application you will see the HandleErrorAttribute
is added to the GlobalFiltersCollection
in theGlobal.asax.cs.
Hide Copy Code
public static void RegisterGlobalFilters(GlobalFiltersCollection filters)
{
filters.Add(new HandleErrorAttribute());
}
Listing 2. Registering HandleErrorAttribute to GlobalFiltersCollection
What the HandleError filter does?
The
HandleError
filter handles the exceptions that are raised by the controller actions, filters and views, it returns a custom view named Error
which is placed in the Shared
folder. The HandleError
filter works only if the <customErrors>
section is turned on in web.config.
The
HandleError
filter handle exceptions only if the <customErrors>
is turned on in web.config.Error View
The Error view that is created by default contains the following HTML:
Hide Copy Code
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Error</title>
</head>
<body>
<h2>
Sorry, an error occurred while processing your request.
</h2>
</body>
</html>
Listing 3. Error View
It contains nothing other than a simple text message. The
Layout
property is set to null so that the Error
view doesn't inherits the application's style.Accessing the exception details in Error view
In some cases, we have to access the exception information from the
Error
view. The HandleError
filter not only just returns the Error
view but it also creates and passes the HandleErrorInfo
model to the view. TheHandleErrorInfo
model contains the details about the exception and the names of the controller and action that caused the exception.
Here is the definition of the
HandleErrorInfo
.
Hide Copy Code
public class HandleErrorInfo
{
public HandleErrorInfo(Exception exception, string controllerName,
string actionName);
public string ActionName { get; }
public string ControllerName { get; }
public Exception Exception { get; }
}
Listing 4. HandleErrorInfo model
Though it's not necessary, let's strongly type the
Error
view to the HandleErrorInfo
model.
Hide Copy Code
@model System.Web.Mvc.HandleErrorInfo
Listing 5. Strongly typing Error view to HandleErrorInfo model
We can easily show the exception and other information by accessing the model through the
Model
property.
Hide Copy Code
<h2>Exception details</h2>
<p>
Controller: @Model.ControllerName <br>
Action: @Model.ActionName
Exception: @Model.Exception
</p>
Listing 6. Displaying exception details in Error view
Returning different views for different exceptions
We can return different views from the
HandleError
filter. For ex. if you want to return one view for database exceptions and another view for application exceptions, you could easily do that by specifying the View
andException
properties as shown in the below listing.
Hide Copy Code
[HandleError(Exception = typeof(DbException), View = "DatabaseError")]
[HandleError(Exception = typeof(AppException), View = "ApplicationError")]
public class ProductController
{
}
Listing 7. Setting View and Exception properties
The
HandleErrorAttribute
can be applied multiple times over a controller or action or at global level.Limitations of HandleError
The
HandleError
filter has some limitations by the way.- Not support to log the exceptions
- Doesn't catch HTTP exceptions other than 500
- Doesn't catch exceptions that are raised outside controllers
- Return error view even for exceptions occurred in AJAX calls
Let us see one by one!
1. Not support to log the exceptions
Logging is very important in error handling and even simple applications get much benefit by logging the errors to a text file or database. The
HandleError
filter suppresses the exceptions once they are handled and all it does is showing a custom view to the user.2. Doesn't catch HTTP exceptions other than 500.
The
HandleError
filter captures only the HTTP exceptions having status code 500 and by-passes the others. Let's assume we have an action that returns all the posts published for a particular category. If the category not exists then we are throwing a 404 HTTP exception.
Hide Copy Code
public ViewResult Posts(string category)
{
if(_blogRepository.Category(category) == null)
throw new HttpException(404, "Category not found");
return _blogRepository.Posts(category);
}
Listing 8. Throwing HttpException from action
If any user passes an invalid category a 404 exception will be thrown from the action and the
HandleError
don't catch this error. Usually in this case, programmers like to show a custom view with a message "The requested category is not found or invalid". Not only the 404 exceptions, the HandleError
doesn't catch any HTTP exception other than 500.
Handling the HTTP exceptions in filters is right or wrong is a thing for debate. In some cases we may have to bypass the HTTP exceptions to the framework to take proper action against it. So handling HTTP exceptions in filters depends upon the application and it's not mandatory!
3. Doesn't catch exceptions that are raised outside controllers
The
HandleError
is an exception filter and exception filters are called only if any exception happens inside the action or in the action filters that are applied over the action. So if the exception happens some other place the filter will be silent.
For example, say we have set up a route constraint for a specific route as shown in the below listing.
Hide Copy Code
routes.MapRoute(
"Default",
"Strange~Action",
new { controller = "NonIE", action = "Action" },
new { browserConstraint = new UserAgentConstraint("IE") }
);
Listing 9. Adding constraints to routes
Route Constraints
restricts the set of URLs that a route will match against.
The
UserAgentConstraint
that we set up in the above route restricts the route to handle requests from only Internet Explorer browsers. In the implementation of UserAgentConstraint
I'm purposely throwing an exception.
Hide Copy Code
public class UserAgentConstraint : IRouteConstraint
{
private readonly string _restrictAgent;
public UserAgentConstraint(string restrictAgent)
{
_restrictAgent = restrictAgent;
}
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
// TODO: return true if the browser is not restricted one.
throw new NotImplementedException("You forget to implement me");
}
}
Listing 9. UserAgentConstraint
This exception is thrown at very early stage and
HandleError
filter won't be there to catch this. When we access the route where the above constraint is applied we will see the yellow screen instead of the custom error page.4. Return error view even in exceptions occurred in AJAX calls.
In case of AJAX calls, if some exception happens the
HandleError
filter returns the custom error view, which is not useful in the client-side. It would be great to return a piece of JSON in AJAX exceptions and for that we have to extend the HandleError
filter or have to create a custom exception filter.HandleError vs. Application_Error
Exception filters are not global error handlers and this is an important reason that forces us to still rely on
Application_Error
event. Some programmers don't even use the HandleError
filter in their application at all and use only the Application_Error
event for doing all the error handling and logging work. The important problem we face in the Application_Error
event is, once the program execution reaches this point then we are out of MVC and we can't access the context objects and other useful stuff of the framework.
Another important feature that exception filters brings to us is we can handle the exceptions in different ways at different scopes, this is important in some cases, for ex. when exceptions are raised from one controller we have to return a custom error view and for other controllers we have to return a different error view, this could be easily accomplished through exception filters but not easily through the
Application_Error
event.
The bottom-line is, we need to use the
Application_Error
event at most of the cases in applications unless we are using a framework like ELMAH which magically handles all the exceptions. But whether we need to use the HandleError
filter or not is totally depend upon the application. When we need a controller or action level exception handling then we can use the HandleError
filter along with the Application_Error
event, else we can simply ignore the HandleError
filter.Extending HandleError
Most of the cases we have to extend the built-in
HandleError
filter or have to create a custom exception filter to do some useful job like logging. Here is an example that shows how to extend the built-in filter to log the exceptions using log4net
and return a JSON object for AJAX calls.
Hide Shrink Copy Code
public class CustomHandleErrorAttribute : HandleErrorAttribute
{
private readonly ILog _logger;
public CustomHandleErrorAttribute()
{
_logger = LogManager.GetLogger("MyLogger");
}
public override void OnException(ExceptionContext filterContext)
{
if (filterContext.ExceptionHandled || !filterContext.HttpContext.IsCustomErrorEnabled)
{
return;
}
if (new HttpException(null, filterContext.Exception).GetHttpCode() != 500)
{
return;
}
if (!ExceptionType.IsInstanceOfType(filterContext.Exception))
{
return;
}
// if the request is AJAX return JSON else view.
if (filterContext.HttpContext.Request.Headers["X-Requested-With"] == "XMLHttpRequest")
{
filterContext.Result = new JsonResult
{
JsonRequestBehavior = JsonRequestBehavior.AllowGet,
Data = new
{
error = true,
message = filterContext.Exception.Message
}
};
}
else
{
var controllerName = (string)filterContext.RouteData.Values["controller"];
var actionName = (string)filterContext.RouteData.Values["action"];
var model = new HandleErrorInfo(filterContext.Exception, controllerName, actionName);
filterContext.Result = new ViewResult
{
ViewName = View,
MasterName = Master,
ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
TempData = filterContext.Controller.TempData
};
}
// log the error using log4net.
_logger.Error(filterContext.Exception.Message, filterContext.Exception);
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusCode = 500;
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
}
}
Listing 10. Extending HandleError
Most of the code are same as in the built-in filter. Notice we have ignored the HTTP exceptions so anyway we need to wire-up the
Application_Error
event to catch and log the missed exceptions. If the request is an AJAX call then we are returning a JSON object that contains a boolean and the exception message else we are returning the error view. We are setting the response status code as 500 and the HandleError
filter also does the same, this is important in terms of REST and HTTP standards.Returning views from Application_Error
In some applications we have to depend upon the
Application_Error
event for handling all the exceptions or the ones that are missed by the exception filters. Mostly programmers like to return an MVC view instead of a static page. Though we are out of the MVC context still we can return a view using a controller (thanks to StackOverflow).
Let's create an
Error
controller that return different views for different errors as shown in the below listing.
Hide Copy Code
public class ErrorController : Controller
{
public ActionResult Index()
{
return View();
}
public ActionResult NotFound()
{
return View();
}
}
Listing 11. Error Controller
We have to invoke this
Error
controller from the Application_Error
to return a view after the exception is logged.
Hide Shrink Copy Code
protected void Application_Error(object sender, EventArgs e)
{
var httpContext = ((MvcApplication)sender).Context;
var currentRouteData = RouteTable.Routes.GetRouteData(new HttpContextWrapper(httpContext));
var currentController = " ";
var currentAction = " ";
if(currentRouteData != null)
{
if(currentRouteData.Values["controller"] != null && !String.IsNullOrEmpty(currentRouteData.Values["controller"].ToString()))
{
currentController = currentRouteData.Values["controller"].ToString();
}
if(currentRouteData.Values["action"] != null && !String.IsNullOrEmpty(currentRouteData.Values["action"].ToString()))
{
currentAction = currentRouteData.Values["action"].ToString();
}
}
var ex = Server.GetLastError();
var controller = new ErrorController();
var routeData = new RouteData();
var action = "Index";
if (ex is HttpException)
{
var httpEx = ex as HttpException;
switch (httpEx.GetHttpCode())
{
case 404:
action = "NotFound";
break;
// others if any
default:
action = "Index";
break;
}
}
httpContext.ClearError();
httpContext.Response.Clear();
httpContext.Response.StatusCode = ex is HttpException ? ((HttpException)ex).GetHttpCode() : 500;
httpContext.Response.TrySkipIisCustomErrors = true;
routeData.Values["controller"] = "Error";
routeData.Values["action"] = action;
controller.ViewData.Model = new HandleErrorInfo(ex, currentController, currentAction);
((IController)controller).Execute(new RequestContext(new HttpContextWrapper(httpContext), routeData));
}
Listing 12. Returning views from Application_Error
We are doing many things in the above code, mainly we are instantiating the
Error
controller and invoking it by calling the Execute()
method passing the HandleErrorInfo
model and this is the model used by theHandleError
filter as well.
We are using a switch statement just to demonstrate how we can return different error views for different HTTP exceptions. If you want to take care of AJAX calls you have to change the implementation little as we did in the custom
HandleError
filter but to keep things simple I've ignored that part.ELMAH
ELMAH (Error Logging Modules and Handlers) is an application-wide error logging facility that is completely pluggable. You can easily configure ELMAH for an ASP.NET MVC application without much code. The other benefits brought up by ELMAH is, it provides a custom page where the admin can view the errors including the original yellow screen, also it provides options to send emails, generate RSS etc.
ELMAH logs only unhandled exceptions so we have to signal ELMAH to log the exceptions that are handled. When we use the
HandleError
filter and ELMAH in an application we will confused seeing no exceptions are logged by ELMAH, it's because once the exceptions are handled by the HandleError
filter it sets theExceptionHandled
property of the ExceptionContext
object to true and that hides the errors from logged by ELMAH. A better way to overcome this problem is extend the HandleError
filter and signal to ELMAH as shown in the below listing.
Hide Copy Code
public class ElmahHandleErrorAttribute : HandleErrorAttribute
{
public override void OnException(ExceptionContext filterContext)
{
base.OnException(filterContext);
// signal ELMAH to log the exception
if (filterContext.ExceptionHandled)
ErrorSignal.FromCurrentContext().Raise(filterContext.Exception);
}
}
Listing 13. Signaling to Elmah
Summary
In this article we saw about the built-in
HandleError
filter available with ASP.NET MVC to handle the exceptions. The HandleError
filter has some limitations and most importantly it doesn't handle all the exceptions that are raised by the application.
Although
HandleError
filter bring some benefits in customizing error handling at a controller or action level still we have to rely on the Application_Error
event.
ELMAH is an error handling module that is easily pluggable to an ASP.NET application. Unlike
HandleError
, ELMAH catches all the exceptions raised by the application.
No comments:
Post a Comment