ASP.NET Core

Global Error Handling in ASP.NET Core MVC

When generating a new MVC app the ExceptionHandler middleware is provided by default. This will catch any unhandled exceptions within your application and allow you to redirect to a specified route if you so choose. But what about other non-success status codes? Errors such as 404s will not be captured by this middleware as no exception was thrown.

To handle these types of errors you will need to use the StatusCodePages middleware. In this post I'm going to cover how to setup an MVC application to handle both exceptions as well as non-success status codes.

Handling Exceptions

I'm going to start here as the majority of the work is already done by the out of the box template. When you create a new MVC application you will get a Startup.cs with a Configure method which looks like this.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    app.UseStaticFiles();
    app.UseCookiePolicy();

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

The line that is important here is app.UseExceptionHandler("/Home/Error"). This statement is registering the ExcepionHandler middleware and it is going to direct the user to the /Home/Errors route whenever an unhandled exception occurs.

All I'm going to do is make a small change so the line reads as follows.

app.UseExceptionHandler("/Error/500");

With that small change in place the next thing I'm going to do is add a new controller, ErrorsController, which is going to handle all the errors from the application.

public class ErrorsController : Controller
{
    [Route("Error/500")]
    public IActionResult Error500()
    {
        return View();
    }
}

NOTE: Do not add HTTP method attributes to the error action method. Using explicit verbs can stop some errors reaching the method.

I'm using attribute routing here as it's my personal preference feel free to use the default route templates if you prefer.

Next I want to be able to get some decent information about what went wrong in my application so I can log it or email it or do any other logic I may deem necessary. In order to do this I'm going to add the following line to my Error500 action.
Note: You will also need to import the Microsoft.AspNetCore.Diagnostics namespace.

var exceptionData = HttpContext.Features.Get<IExceptionHandlerPathFeature>();

When the ExceptionHandler middleware runs it sets an item on the Features collection for the request called IExceptionHandlerPathFeature. This is one of 2 features added the other is IExceptionHandlerFeature. They both contain a property called Error which has the details of the Exception. But the IExceptionHandlerPathFeature also contains the path from which the exception was thrown. Based on this I would always recommend using IExceptionHandlerPathFeature.

Now I have some information about what went wrong I want to do something with it. Now for the sake for this post I'm just going to add some details to the ViewBag so I can show them on a view. However in a real application I would most likely want to log them and then show the user a friendlier screen.

[Route("Error/500")]
public IActionResult Error500()
{
    var exceptionFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();

    if (exceptionFeature != null)
    {
        ViewBag.ErrorMessage = exceptionFeature.Error.Message;
        ViewBag.RouteOfException = exceptionFeature.Path;
    }

    return View();
}

I can now handle any unhandled exceptions that my application throws, then print out the details. Next I want to deal with non-exception based issues, things such as 404s or any other non-success status code my app may produce.

Non-success Status Codes

The StatusCodePages middleware deals with any status codes returned by the app that are between 400 and 599 and don't have a body. There are three different extensions for the middleware available.

  • UseStatusCodePages
  • UseStatusCodePagesWithRedirects
  • UseStatusCodePagesWithReExecute

UseStatusCodePages

This is the simplest extension. When this is added to the pipeline any status code produced which matches the criteria above will be intercepted and a simple text response will be returned to the caller. Below is an example of what would be returned if a request was made for a page that didn't exist.

Status Code: 404; Not Found

While this may have its uses, in reality you are probably going to want to do something a bit more sophisticated.

UseStatusCodePagesWithRedirect

This extension will allow you to configure a user friendly error page rather than just the plain text option above.

This extension and the next are extremely similar in how they work except for one key difference. This will redirect the response to the error page location however, in doing so the original error response is lost. The caller will see a 200 status code from the loading of the error page but not the actual status code which triggered the error.

Now this may not matter to you but it is technically wrong as you will be returning a success status code when there was actually an error. You will have to decided if this is OK for your use case.

UseStatusCodePagesWithReExecute

This is the configuration I will be using, it's also the one I would suggest is best for most cases. The middleware will pick up any matching status codes being returned and then re-execute the pipeline. So when the user friendly error page is returned the correct error status code is returned as well.

I'm going to add the following line underneath the ExceptionHandler middleware from earlier.

app.UseStatusCodePagesWithReExecute("/Error/{0}");

I've used the {0} placeholder when defining my Error route. This will be populated with the status code which triggered the middleware. I can then pick this up in an action on the ErrorsController.

[Route("Error/{statusCode}")]
public IActionResult HandleErrorCode(int statusCode)
{
    var statusCodeData = HttpContext.Features.Get<IStatusCodeReExecuteFeature>();

    switch (statusCode)
    {
        case 404:
            ViewBag.ErrorMessage = "Sorry the page you requested could not be found";
            ViewBag.RouteOfException = statusCodeData.OriginalPath;
            break;
        case 500:
            ViewBag.ErrorMessage = "Sorry something went wrong on the server";
            ViewBag.RouteOfException = statusCodeData.OriginalPath;
            break;
    }

    return View();
}

Much like with the ExceptionHandler middleware the StatusCodePages middleware populates an object to give a bit more information about whats happened. This time I can request a IStatusCodeReExecuteFeature from the current requests Features collection. With this I can then access three properties.

  • OriginalPathBase
  • OriginalPath
  • OriginalQueryString

This allows me access to the route that triggered the status code along with any querystring data that may be relevant. Again for the purposes of this post I am just setting some ViewBag data to pass down to the view. But in a real world application I would be logging this information somewhere.

In the example above I have defined a single action to handle all status codes. But you could quite easily define different actions for different status codes. If, for example, you wanted to have a dedicated endpoint for 404 status codes you could define it as follows:

[Route("Error/404")]
public IActionResult HandlePageNotFound()
{
    ...
}

Wrapping up

In this post I've gone over a couple of pieces of middleware you can use for global error handling in your ASP.NET Core MVC applications. As always if you have any questions please ask away in the comments and I will do my best to answer them.