There are several ways to handle uncaught exceptions in ASP.NET MVC. We could use the default
HandleErrorAttribute filter, extend the filter, create our own error filter, override
OnException method in controller or use
Application_Error method in
global.asax.
Using Default MVC Error Handler
By default an MVC project applies
HandleErrorAttribute filter globally in
global.asax.
public static void RegisterGlobalFilters(GlobalFiltersCollection filters)
{
filters.Add(new HandleErrorAttribute());
}
It returns the default error view (
Error.cshtml) created inside
Views\Shared folder.
To have this error handler working,
CustomErrors must be set to 'On' in
web.config:
<customErrors mode="On">
</customErrors>
Using HandleErrorAttribute Filter in Controller or Action
If we only want to apply the
HandleError filter in some controllers or actions, then we need to remove
HandleErrorAttribute filter addition in
RegisterGlobalFilters method in
global.asax. Then apply the filter on specific controllers or actions only.
We can simply use this one:
[HandleError]
This will handle all possible errors it could catch and show the default error view (
Error.cshtml) located inside
Views\Shared folder.
Otherwise we can specify what type of error to be handled and what view to be displayed. For example:
[HandleError(ExceptionType=typeof(ArgumentException), View="ArgumentError")]
We can also stack these filters to handle different types of error on controllers or actions.
This filter only catches errors originated from inside controller actions and other filters applied to them. It also only handles HTTP 500 Internal Server error. The filter does not do much. After catching errors, it will only show the error page.
Apart from this filter, if we want to catch other than HTTP 500 error, we can set the
customErrors in the config file for a particular view to be loaded when the error happens. This is commonly used for HTTP 404 Not Found error:
<customerrors mode="On">
<error statuscode="404" redirect="~/Error/PageNotFound">
</error>
</customerrors>
Extending HandleErrorAttribute Filter
By extending this filter, we can add more capabilities such as to log error and handle errors generated from AJAX requests. Below is an example:
public class ExtendedHandleErrorAttribute : HandleErrorAttribute
{
public override void OnException(ExceptionContext filterContext)
{
// if exception is handled already
if (filterContext.ExceptionHandled || !filterContext.HttpContext.IsCustomErrorEnabled)
{
return;
}
// pass other HTTP exceptions to global application error handler
if (new HttpException(null, filterContext.Exception).GetHttpCode() != 500)
{
return;
}
if (!ExceptionType.IsInstanceOfType(filterContext.Exception))
{
return;
}
// if the request is AJAX then return JsonResult else normal view
if (filterContext.HttpContext.Request["X-Requested-With"] == "XMLHttpRequest" || ((filterContext.HttpContext.Request.Headers != null) && (filterContext.HttpContext.Request.Headers["X-Requested-With"] == "XMLHttpRequest")))
{
filterContext.Result = new JsonResult
{
JsonRequestBehavior = JsonRequestBehavior.AllowGet,
Data = new
{
error = true,
message = filterContext.Exception.Message
}
};
}
else
{
base.OnException(filterContext);
}
// log the error
//. . .
}
}
In this example, we are just handling HTTP 500 error (Internal Server error) to comply with HTTP standards. Other errors will be passed to global application error handler (
Application_Error() method). If you'd like to return a different view, please see that part of codes in the next example (creating custom error filter).
Creating Custom Error Filter
If extending
HandleErrorAttribute above is not enough then we can create our own custom error filter. The filter class will inherit from
FilterAttribute and
IExceptionFilter.
public class CustomHandleErrorAttribute : FilterAttribute, IExceptionFilter
{
public void OnException(ExceptionContext filterContext)
{
// if exception is handled already
if (filterContext.ExceptionHandled || !filterContext.HttpContext.IsCustomErrorEnabled)
{
return;
}
// pass other HTTP exceptions to global application error handler
if (new HttpException(null, filterContext.Exception).GetHttpCode() != 500)
{
return;
}
/*if (!ExceptionType.IsInstanceOfType(filterContext.Exception))
{
return;
}*/
// if the request is AJAX then return JsonResult else ViewResult
if (filterContext.HttpContext.Request["X-Requested-With"] == "XMLHttpRequest" || ((filterContext.HttpContext.Request.Headers != null) && (filterContext.HttpContext.Request.Headers["X-Requested-With"] == "XMLHttpRequest")))
{
filterContext.Result = new JsonResult
{
JsonRequestBehavior = JsonRequestBehavior.AllowGet,
Data = new
{
error = true,
message = filterContext.Exception.Message
}
};
}
else
{
// if we want to pass detailed error info to the view
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 = "theViewName",
ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
TempData = filterContext.Controller.TempData
};
}
// log the error
//. . .
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusCode = 500;
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
}
}
The example codes handle AJAX and normal requests, log error and return a particular view for a normal request.
Overriding OnException Method in Controller
Another way to handle error in ASP.NET MVC application is by overriding
OnException method in controller. However, like the
HandleErrorAttribute filter above, this method only catches errors that happen inside a controller. Other errors such as data binding or route errors will not be caught. We could also return a view by assigning a
ViewResult to its
ExceptionContext object's
Result property. Below is an example:
protected override void OnException(ExceptionContext context)
{
// pass other errors to global application error handler
if (context.Exception is InvalidOperationException)
{
// do some error logging
//. . .
// if we want to pass detailed error info to the view
var controllerName = (string)context.RouteData.Values["controller"];
var actionName = (string)context.RouteData.Values["action"];
var model = new HandleErrorInfo(context.Exception, controllerName, actionName);
var result = new ViewResult
{
ViewName = "theViewName",
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;
}
}
Using Application Global Error Handler
Lastly, we can also use
Application_Error() method in
global.asax. This method catches all unhandled errors and is the last resort before the yellow error screen. Many developers only use this method alone to handle all kind of errors in the application. When there is a need to handle errors (or some specific errors) at controller or action method level then the other error handlers mentioned above can be used.
Below is an example of using
Application_Error() method and its related error controller and view to catch errors:
global.asax Application_Error() method:
protected void Application_Error(object sender, EventArgs e)
{
HttpContext httpContext = ((MyApplicationName)sender).Context;
Exception exception = Server.GetLastError();
if (httpContext.Request["X-Requested-With"] == "XMLHttpRequest" || ((httpContext.Request.Headers != null) && (httpContext.Request.Headers["X-Requested-With"] == "XMLHttpRequest")))
{
// handle AJAX request
httpContext.ClearError();
Response.StatusCode = 500;
Response.ContentType = "application/json";
Response.StatusDescription = "my custom status description"; // => Response.StatusDescription maps to jqXHR or XMLHttpRequest.statusText
// => Response.Write() maps to jqXHR or XMLHttpRequest.responseText
#if DEBUG
Response.Write(new System.Web.Script.Serialization.JavaScriptSerializer().Serialize(new
{
errorMessage = exception.ToString()
}));
//Response.Write(exception.ToString());
#else
Response.Write("an application error has occurred");
#endif
}
else
{
// non AJAX request
IController errorController = new MyWebProject.Controllers.ErrorController();
#if DEBUG
{
// get information to be passed to view as model
string currentController = string.Empty;
string currentAction = string.Empty;
RouteData currentRouteData = RouteTable.Routes.GetRouteData(new HttpContextWrapper(httpContext));
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();
}
}
((Controller)errorController).ViewData.Model = new HandleErrorInfo(exception, currentController, currentAction);
}
#else
{
// only show a message
((Controller)errorController).ViewData.Model = "error message . . .";
}
#endif
string action = "Error";
if (exception is HttpException)
{
// get the action for different error codes
switch (((HttpException)exception).GetHttpCode())
{
case 404:
action = "NotFound";
break;
// other errors
}
}
httpContext.ClearError();
httpContext.Response.Clear();
httpContext.Response.StatusCode = exception is HttpException ? ((HttpException)exception).GetHttpCode() : 500;
// avoid IIS7 getting involved
httpContext.Response.TrySkipIisCustomErrors = true;
// execute the error controller
RouteData routeData = new RouteData();
routeData.Values["controller"] = "Error";
routeData.Values["action"] = action;
//IController errorController = new NSWHealth.ICPBS.Web.Controllers.ErrorController(); // for readability only, this is already done above
//((Controller)errorController).ViewData.Model = new HandleErrorInfo(exception, currentController, currentAction); // for readability only, this is already done above
errorController.Execute(new RequestContext(new HttpContextWrapper(httpContext), routeData));
}
}
error controller:
public class ErrorController : Controller
{
public ActionResult Error()
{
return View();
}
public ViewResult NotFound()
{
//Response.StatusCode = 404; //could be set to 200 as well
return View("NotFound");
}
}
error view:
@{
ViewBag.Title = "Error";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Error</h2>
<div>
@if (Html.IsDebug())
{
<div>
<p>
<b>Exception:</b> @Model.Exception.Message<br />
<b>Controller:</b> @Model.ControllerName<br />
<b>Action:</b> @Model.ActionName
</p>
<div style="overflow:scroll">
<pre>
@Model.Exception.StackTrace
</pre>
</div>
</div>
}
else
{
<div style="min-height:460px">
An error has occurred: @Model
</div>
}
</div>
References and further reading:
Handling Errors Effectively in ASP.NET MVC
Exception Handling in ASP.NET MVC
Exception Handling in MVC