64

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?

Matthew Manela
  • 16,572
  • 3
  • 64
  • 66
Adrian Anttila
  • 2,038
  • 5
  • 22
  • 25

8 Answers8

63

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);
}
splattne
  • 102,760
  • 52
  • 202
  • 249
  • Yep, this is the way to do it. Using one of the provided filters, or creating your own exception filter is the way to go. You can change the view based on the type of exception or dive deeper by inspecting more info in your custom filter. – a7drew May 24 '09 at 04:13
  • Are you sure this wasn't something that was in preview releases of MVC but was changed for 1.0? I don't see this override, instead it looks like you should override OnActionExecuted and inspect the filterContext.Exception value – Richard Ev Jan 06 '10 at 14:11
  • http://msdn.microsoft.com/en-us/library/system.web.mvc.controller.onexception(VS.100).aspx –  Jan 06 '10 at 15:58
  • 6
    You will want similar code in the global.asax file for Application_Error to make sure you handle exceptions that occur outside of the controllers (e.g., routing exception for having a request match multiple routes). It seems less sketchy handling exceptions in the controller; even if it works, something doesn't feel right about Server.GetLastError on a server handling simultaneous requests. Having similar code in global.asax will alert you to all your exceptions. – patridge Feb 22 '11 at 20:27
  • I can't stress enough that this will NOT handle 404's when the controller is not found. Refer to @Adrian Anttila's answer, below. – Pure.Krome Aug 06 '11 at 12:04
  • 1
    @Chandana Dunno, offhand. If you can't find it in the msdn docs, then it's just a poco that holds information about the exception used to display that content in a web page. –  Apr 30 '16 at 16:39
  • Can `filterContext` or `filterContext.Exception` actually be null? Considering on an excpetion. – Efreeto Dec 19 '19 at 23:28
21

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.

Matthew Manela
  • 16,572
  • 3
  • 64
  • 66
Adrian Anttila
  • 2,038
  • 5
  • 22
  • 25
  • 1
    I would suggest you to check the source of http://kigg.codeplex.com where you will find the complete source code of this httpModule, I just want to clutter global.asax that is why the HttpModule. – Kazi Manzur Rashid May 01 '09 at 20:50
13

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;
        }
Craig Stuntz
  • 125,891
  • 12
  • 252
  • 273
  • You've pointed out that one of my statements is incorrect without addressing the real issue of not being able to provide error handling for HTTP error codes. – Adrian Anttila May 01 '09 at 17:42
  • 4
    I can explain what is happening, but I would need more information in order to tell you the right thing to do for your application. HandleError works on an action, and you don't generally throw HttpExceptions within an action. Rather than guess what you are actually trying to do and give you potentially incorrect advice, I can just explain the facts behind the behavior you're seeing. Please feel free to update the question if you'd like to explain more about what you really want to do. – Craig Stuntz May 01 '09 at 18:20
6

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);
    }
}
}
Nimantha
  • 6,405
  • 6
  • 28
  • 69
Kazi Manzur Rashid
  • 1,544
  • 13
  • 14
  • This is what I was looking for. I took a slightly simpler approach and put my code in Application_Error to handle the same set of errors across controllers. I'll post the solution in an answer to this question. – Adrian Anttila May 01 '09 at 19:47
3

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.

Simon_Weaver
  • 140,023
  • 84
  • 646
  • 689
  • You could also declare the [HandleError] attribute on your controllers or controller methods, but it doesn't help for HTTP errors. – Adrian Anttila May 21 '09 at 20:29
2

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);
Nimantha
  • 6,405
  • 6
  • 28
  • 69
mindplay.dk
  • 7,085
  • 3
  • 44
  • 54
  • It seems like you are replicating the behaviour already provided for by the HandleError attribute. – Martin Suchanek Aug 17 '10 at 17:32
  • 2
    Possibly. Learning all of this stuff is turning out to be a painful effort. So many bad examples and outdated documentation to filter through. I guess I just contributed another bad example ;-) – mindplay.dk Sep 22 '10 at 02:07
2
     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);
        }
    }
crypted
  • 10,118
  • 3
  • 39
  • 52
bharat
  • 21
  • 1
  • 1
    You may want to store the result of `this.View("Error")` in a local variable instead of invoking the method 2 times. – SandRock Apr 27 '12 at 10:59
1

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
Martin_W
  • 1,582
  • 1
  • 19
  • 24