Error Handling in ASP.NET MVC

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?


Solution 1:

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:

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:

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;
        }