We just recently saw people ask this questions on forums, so we decide to write short brief tutorial to make custom error pages in ASP.NET MVC Core.

So, let’s start how to implement custom error pages 404s and exceptions on this blog.

Custom error pages

Anyone know what is custom error pages? It is great looking view when something goes wrong in a production environment.

In development you would use:

app.UseDeveloperExceptionPage();

Now when an exception occurs, you will see a view that describes what went wrong and where.

But it should not be used in production as it could lead to security problems. Users might also consider your site less stable, and never visit again.

So we want to show them something that says something like:

Oops! Something went wrong. We are very sorry, please try again in a moment. If the error persists...

In case of a 404 status code, by default ASP.NET Core just returns a white page.

Instead we would like to show something like:

Hey that thing you tried to access does not exist. Please check the URL is correct.

Exception handler middleware

Let's start by handling exceptions properly.

In our application pipeline configuration, we will add the exception handler middleware:

if(!env.IsDevelopment())
{
    app.UseExceptionHandler("/error/500");
}

So what does this middleware do?

Well, put simply it:

  1. Calls the next middleware with a try-catch block
  2. If an exception is caught, the next middleware is called again with the request path set to what you gave as an argument

This re-execution means the original URL is preserved in the browser.

The controller and action that is implemented looks like this:

[Route("error")]
public class ErrorController : Controller
{
    private readonly TelemetryClient _telemetryClient;

    public ErrorController(TelemetryClient telemetryClient)
    {
        _telemetryClient = telemetryClient;
    }

    [Route("500")]
    public IActionResult AppError()
    {
        var exceptionHandlerPathFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
        _telemetryClient.TrackException(exceptionHandlerPathFeature.Error);
        _telemetryClient.TrackEvent("Error.ServerError", new Dictionary<string, string>
        {
            ["originalPath"] = exceptionHandlerPathFeature.Path,
            ["error"] = exceptionHandlerPathFeature.Error.Message
        });
        return View();
    }
}

I took a look at the source code of the exception handler middleware, and found that it sets this IExceptionHandlerPathFeature on the context before re-executing the request.

Here we access it to get the relative URL the user tried to access and the exception that occurred. We use Application Insights to track the exception.

The view is pretty simple:

@{
    ViewBag.Title = "Error occurred";
}

<h1>We have a problem</h1>

<p>Sorry, an error occurred while executing your request.</p>

Now when an exception is thrown:

  1. The exception is logged in Application Insights
  2. User gets a nice-looking view instead of a stack trace
  3. The original URL is preserved in the browser so the user can try to refresh
  4. The response comes back with a 500 status code so it is tracked as a failed request in Application Insights

Handling 404s

We also want to handle 404 status codes gracefully.

In this blog, it could happen because the URL did not map to a controller action. Or it might have, but we could not find something in the database.

404s are handled with a small middleware that is placed before the MVC middleware:

app.Use(async (ctx, next) =>
{
    await next();

    if(ctx.Response.StatusCode == 404 && !ctx.Response.HasStarted)
    {
        //Re-execute the request so the user gets the error page
        string originalPath = ctx.Request.Path.Value;
        ctx.Items["originalPath"] = originalPath;
        ctx.Request.Path = "/error/404";
        await next();
    }
});

Re-executing a request is not hard as you can see. We just call await next(); again.

Here we grab the original URL and put it in the HttpContext.Items collection.

This is the action that then handles the error in the ErrorController introduced earlier:

[Route("404")]
public IActionResult PageNotFound()
{
    string originalPath = "unknown";
    if (HttpContext.Items.ContainsKey("originalPath"))
    {
        originalPath = HttpContext.Items["originalPath"] as string;
    }
    _telemetryClient.TrackEvent("Error.PageNotFound", new Dictionary<string, string>
    {
        ["originalPath"] = originalPath
    });
    return View();
}

We track the event with a custom event in Application Insights, and include the original URL in the properties.

The view is quite simple again:

@{
    ViewBag.Title = "404";
}

<h1>404 - Page not found</h1>

<p>Oops, better check that URL.</p>

If you open the browser DevTools, you can again see we get the right status code: 404.

And the original URL is preserved in the browser.

Happy Coding!