Unify Exception handling between Ajax and non Ajax requests in Asp.Net MVC

What is more annoying than a fix web ui? We don’t know if the normal process is in progress, if an error occurred, which error… That’s what happens quite often when Ajax calls throw a server side exception. The exception information is in this case forwarded only in the response not displayed by default to the user and the site just appears to be dead. There are some solutions to handle correctly these errors and we are going to see how we can do in MVC to unify errors display between Ajax and non Ajax requests

With MVC, error handling is often done by the HandleError filter. Unfortunately, this filter doesn’t do anything particular for Ajax requests and just sends the error view in the response. This view is invisible for the user (no, the lambda users don’t look the requests in firebug!) At this point 2 solutions: Explaining the users how to use firebug :p, or code a real web error handling in which Ajax errors are correctly handled. I think everyone will agree for the second solution!

The base idea is to trap all the exceptions, extract all the needed info to build a HandleErrorInfo object, and calling a new controller with our HandleErrorInfo object in his ViewData. For this test project, we will display the exceptions in a jQuery UI popup but you can do everything you want for this part. (you will only have to customize the error view)

Let’s start with our controller:
It will have 2 actions: one for Ajax calls and another for standards calls.

public class ErrorController : Controller
{
    //
    // GET: /Error/

    public ActionResult Index()
    {
        return View("Index",ViewData.Model);
    }

    public ActionResult IndexAjax()
    {
        return PartialView("Detail", ViewData.Model);
    }

}

These actions just render a view with the HandleErrorInfo as the model. For the ajax action, there is already a page in the client browser so we just send a PartialView. For the standard call, we display an error page containing the same partial view as for Ajax calls.

Views are the following:

Detail

@model HandleErrorInfo
<div>Controller: @Model.ControllerName</div>
<div>Action: @Model.ActionName</div>
<div>Message: @Model.Exception.Message</div>

Index

@model HandleErrorInfo
@{
ViewBag.Title = "ERROR";
}

<h2>ERROR</h2>
<div id="error">
@Html.Partial("Detail",Model)
</div>
$(function () {
$("#error").dialog();
});

<a href="history.back()">Go back</a>

But how is the HandleErrorInfo object added into the ViewData?
To do this, we will create a new filter implementing IExceptionFilter. This interface contains only one method OnException with and ExceptionCOntext argument. It is invoked each time an exception in thrown in the MVC process. In our case, this method will have to do a few things:

  • Check if custom errors are activated in the web.config (customErrors). The HttpContext makes it easy by exposing a IsCustomErrorEnabled property.
  • Check whether the exception still needs handling (in case there are several ExceptionFilters defined). This is done via the ExceptionHandled property of the ExceptionContext object. If it is not handled, flag it as handled by our filter
  • Update the response status code to 500
  • Create and fill a HandleErrorInfo object with the ExceptionContext
  • Create an instance of our error controller
  • Invoke the right action (Ajax or not)

Nothing very complicated it looks like this:

public void OnException(ExceptionContext filterContext)
{
    //run any code you need here: logging, alerts...

    if (filterContext.HttpContext.IsCustomErrorEnabled && !filterContext.ExceptionHandled)
    {
        filterContext.ExceptionHandled = true;
        filterContext.HttpContext.Response.StatusCode = 500;
        string controllerName = (string)filterContext.RouteData.Values["controller"];
        string actionName = (string)filterContext.RouteData.Values["action"];
        HandleErrorInfo info = new HandleErrorInfo(filterContext.Exception, controllerName, actionName);

        IControllerFactory factory = ControllerBuilder.Current.GetControllerFactory();
        ErrorController newController = factory.CreateController(filterContext.RequestContext, "Error") as ErrorController;
        filterContext.RouteData.Values["controller"] = "Error";
        filterContext.Controller = newController;
        filterContext.Controller.ViewData = new ViewDataDictionary(info);

        string actionToCall = "Index";
        if (filterContext.HttpContext.Request.IsAjaxRequest())
        {
            actionToCall = "IndexAjax";
        }

        filterContext.RouteData.Values["action"] = actionToCall;
        newController.ActionInvoker.InvokeAction(filterContext, actionToCall);
    }
}

We can see that the controller instance is created via ControllerBuilder and ControllerFactory not to break an existing dependency injection (if you use it in your application). You can find more information on how a controller is created in my previous post.

We just have to register this filter in the GlobalFilters application collection to activate it.
In the global.asax:

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
    //filters.Add(new HandleErrorAttribute());
    filters.Add(new GlobalExceptionFilter());
}

We commented here the default HandleErrorAttribute line. We don’t need it anymore.

The non Ajax error page is now up and running but for the Ajax handling we have one more step to accomplish. The client has received the partial error view but we need to tell the client what it has to do with this information. We will use for this the jQuery method ajaxError.
Add these few lines in your layout:

<div id="error" title="ERROR"></div>
<script language="javascript">
    $(document).ajaxError(function (e, jqxhr, settings, exception) {
        if (jqxhr != null) {
            $("#error").html(jqxhr.responseText);
            $("#error").dialog();
        }
    });
</script>

Each time an ajax request will come back with a 500 status code, this function will be invoked and the jqxhr argument will be filled with the response content (our partial view here). The other arguments contain more information about the error and the current response. In our case, we just have to fill our error div with the content of the partial view and open the popup.

Be careful, if an exception is thrown in the error handling process, the filter will be invoked again and you will have a nice infinite loop…

You can see the code in action HERE or download and example project HERE

Advertisements
This entry was posted in Asp.Net MVC and tagged , , , , , . Bookmark the permalink.

19 Responses to Unify Exception handling between Ajax and non Ajax requests in Asp.Net MVC

  1. Ephraim says:

    Thanks a lot. One question: What does setting the response code to 500 do for us?

  2. Ephraim says:

    This is working very nicely for local users. But whenever a remote user accesses my site and they get an exception they get a 500 error and the exception details are not displayed. It seems that they can’t access the error view (or controller). Any ideas?

  3. Hi!

    This is a nice article. Thank for sharing your knowledge. There are some other links related to “Calling partial view using Ajax in ASP.NET MVC 4”. I hope this is a

    very useful for developers.

    http://www.mindstick.com/Articles/8e131adf-2621-4cd7-8557-770c1ede799c/?Calling%20partial%20view%20using%20Ajax%20in%20ASP.NET%20MVC%204

    http://ciintelligence.blogspot.in/2012/06/aspnet-mvc-ajax-load-with-partial-view.html

  4. Hi, I am already using this technique, but I ended up here looking for a way to show the non ajax exception as an alert on my page. Since it’s a non-ajax request I think it’s not possible. Do you know that? Thanks

  5. Mike E says:

    I implemented this and it’s working great for AJAX requests, but for normal page exceptions, I get a cyclic error after the initial attempt to handle an exception. The initial exception handling of the original controller/action error occurs followed by OnException() being called repeatedly. The subsequent error is “Value cannot be null. Parameter name: controllerContext” on the Error controller’s Index action. Here is the stack:

    at System.Web.Mvc.ChildActionValueProviderFactory.GetValueProvider(ControllerContext controllerContext)
    at System.Web.Mvc.ValueProviderFactoryCollection.c__DisplayClassc.b__7(ValueProviderFactory factory)
    at System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()
    at System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()
    at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
    at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
    at System.Web.Mvc.ValueProviderFactoryCollection.GetValueProvider(ControllerContext controllerContext)
    at System.Web.Mvc.ControllerBase.get_ValueProvider()
    at System.Web.Mvc.ControllerActionInvoker.GetParameterValue(ControllerContext controllerContext, ParameterDescriptor parameterDescriptor)
    at System.Web.Mvc.ControllerActionInvoker.GetParameterValues(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
    at System.Web.Mvc.ControllerActionInvoker.InvokeAction(ControllerContext controllerContext, String actionName)

Answer

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s