Error Handling in ASP.NET MVC

asp.net MvcException

asp.net Mvc Problem Overview


How can I correctly handle exceptions thrown from controllers in ASP.NET MVC? The HandleError attribute seems to only process exceptions thrown by the MVC infrastructure and not exceptions thrown by my own code.

Using this web.config

<customErrors mode="On">
    <error statusCode="401" redirect="/Errors/Http401" />
</customErrors>

with the following code

namespace MvcApplication1.Controllers
{
    [HandleError]
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            // Force a 401 exception for testing
            throw new HttpException(401, "Unauthorized");
        }
    }
}

doesn't result in what I was hoping for. Instead I get the generic ASP.NET error page telling me to modify my web.config to see the actual error information. However, if instead of throwing an exception I return an invalid View, I get the /Shared/Views/Error.aspx page:

return View("DoesNotExist");

Throwing exceptions within a controller like I've done above seems to bypass all of the HandleError functionality, so what's the right way to create error pages and how do I play nice with the MVC infrastructure?

asp.net Mvc Solutions


Solution 1 - asp.net Mvc

Controller.OnException(ExceptionContext context). Override it.

protected override void OnException(ExceptionContext filterContext)
{
	// Bail if we can't do anything; app will crash.
	if (filterContext == null)
		return;
		// since we're handling this, log to elmah
	
    var ex = filterContext.Exception ?? new Exception("No further information exists.");
	LogException(ex);
	
    filterContext.ExceptionHandled = true;
	var data = new ErrorPresentation
		{
			ErrorMessage = HttpUtility.HtmlEncode(ex.Message),
			TheException = ex,
			ShowMessage = !(filterContext.Exception == null),
			ShowLink = false
		};
	filterContext.Result = View("ErrorPage", data);
}

Solution 2 - asp.net Mvc

Thanks to kazimanzurrashaid, here is what I wound up doing in Global.asax.cs:

protected void Application_Error()
{
    Exception unhandledException = Server.GetLastError();
    HttpException httpException = unhandledException as HttpException;
    if (httpException == null)
    {
        Exception innerException = unhandledException.InnerException;
        httpException = innerException as HttpException;
    }
            
    if (httpException != null)
    {
        int httpCode = httpException.GetHttpCode();
        switch (httpCode)
        {
            case (int) HttpStatusCode.Unauthorized:
                Response.Redirect("/Http/Error401");
                break;
        }
    }
}

I'll be able to add more pages to the HttpContoller based on any additional HTTP error codes I need to support.

Solution 3 - asp.net Mvc

> The HandleError attribute seems to only process exceptions thrown by the MVC infrastructure and not exceptions thrown by my own code.

That is just wrong. Indeed, HandleError will only "process" exceptions either thrown in your own code or in code called by your own code. In other words, only exceptions where your action is in the call stack.

The real explanation for the behavior you're seeing is the specific exception you're throwing. HandleError behaves differently with an HttpException. From the source code:

        // If this is not an HTTP 500 (for example, if somebody throws an HTTP 404 from an action method),
        // ignore it.
        if (new HttpException(null, exception).GetHttpCode() != 500) {
            return;
        }

Solution 4 - asp.net Mvc

I don't think you will be able to show specific ErrorPage based upon the HttpCode with the HandleError Attribute and I would prefer to use an HttpModule for this purpose. Assuming that I have folder "ErrorPages" where different page exists for each specific error and the mapping is specifed in the web.config same as the regular web form application. And the following is the code which is used to show the error page:

public class ErrorHandler : BaseHttpModule{

public override void OnError(HttpContextBase context)
{
    Exception e = context.Server.GetLastError().GetBaseException();
    HttpException httpException = e as HttpException;
    int statusCode = (int) HttpStatusCode.InternalServerError;

    // Skip Page Not Found and Service not unavailable from logging
    if (httpException != null)
    {
        statusCode = httpException.GetHttpCode();

        if ((statusCode != (int) HttpStatusCode.NotFound) && (statusCode != (int) HttpStatusCode.ServiceUnavailable))
        {
            Log.Exception(e);
        }
    }

    string redirectUrl = null;

    if (context.IsCustomErrorEnabled)
    {
        CustomErrorsSection section = IoC.Resolve<IConfigurationManager>().GetSection<CustomErrorsSection>("system.web/customErrors");

        if (section != null)
        {
            redirectUrl = section.DefaultRedirect;

            if (httpException != null)
            {
                if (section.Errors.Count > 0)
                {
                    CustomError item = section.Errors[statusCode.ToString(Constants.CurrentCulture)];

                    if (item != null)
                    {
                        redirectUrl = item.Redirect;
                    }
                }
            }
        }
    }

    context.Response.Clear();
    context.Response.StatusCode = statusCode;
    context.Response.TrySkipIisCustomErrors = true;

    context.ClearError();

    if (!string.IsNullOrEmpty(redirectUrl))
    {
        context.Server.Transfer(redirectUrl);
    }
}
}

Solution 5 - asp.net Mvc

One other possibility (not true in your case) that others reading this may be experiencing is that your error page is throwing an error itself, or is not implementing :

 System.Web.Mvc.ViewPage<System.Web.Mvc.HandleErrorInfo>

If this is the case then you will get the default error page (otherwise you'd get an infinite loop because it would keep trying to send itself to your custom error page). This wasn't immediately obvious to me.

This model is the model sent to the error page. If your error page uses the same master page as the rest of your site and requires any other model information then you will need to either create your own [HandleError] type of attribute or override OnException or something.

Solution 6 - asp.net Mvc

I chose the Controller.OnException() approach, which to me is the logical choice - since I've chosen ASP.NET MVC, I prefer to stay at the framework-level, and avoid messing with the underlying mechanics, if possible.

I ran into the following problem:

If the exception occurs within the view, the partial output from that view will appear on screen, together with the error-message.

I fixed this by clearing the response, before setting filterContext.Result - like this:

        filterContext.HttpContext.Response.Clear(); // gets rid of any garbage
        filterContext.Result = View("ErrorPage", data);

Solution 7 - asp.net Mvc

     protected override void OnException (ExceptionContext filterContext )
    {
        if (filterContext != null && filterContext.Exception != null)
        {
            filterContext.ExceptionHandled = true;
            this.View("Error").ViewData["Exception"] = filterContext.Exception.Message;
            this.View("Error").ExecuteResult(this.ControllerContext);
        }
    }

Solution 8 - asp.net Mvc

Jeff Atwood's User Friendly Exception Handling module works great for MVC. You can configure it entirely in your web.config, with no MVC project source code changes at all. However, it needs a small modification to return the original HTTP status rather than a 200 status. See this related forum post.

Basically, in Handler.vb, you can add something like:

' In the header...
Private _exHttpEx As HttpException = Nothing

' At the top of Public Sub HandleException(ByVal ex As Exception)...
HttpContext.Current.Response.StatusCode = 500
If TypeOf ex Is HttpException Then
    _exHttpEx = CType(ex, HttpException)
    HttpContext.Current.Response.StatusCode = _exHttpEx.GetHttpCode()
End If

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAdrian AnttilaView Question on Stackoverflow
Solution 1 - asp.net Mvcuser1228View Answer on Stackoverflow
Solution 2 - asp.net MvcAdrian AnttilaView Answer on Stackoverflow
Solution 3 - asp.net MvcCraig StuntzView Answer on Stackoverflow
Solution 4 - asp.net MvcKazi Manzur RashidView Answer on Stackoverflow
Solution 5 - asp.net MvcSimon_WeaverView Answer on Stackoverflow
Solution 6 - asp.net Mvcmindplay.dkView Answer on Stackoverflow
Solution 7 - asp.net MvcbharatView Answer on Stackoverflow
Solution 8 - asp.net MvcMartin_WView Answer on Stackoverflow