Creating custom Error pages and catching Exceptions in ASP.NET Core 2.0

One of my first tasks on the project was to create a custom error 404 page that the users of our application will be redirected to in case of such an error. By default, an ASP.NET Core application will redirect you to a browser-specific error page. That’s not really helpful to the user, since on Mozilla Firefox the page will be simply blank. Other browsers’ versions aren’t pretty either and in general you as the developer want to provide the user with some information on why the error occurred and maybe show him/her a link to the Home page and such. Shortly after setting the custom 404 page I’ve realized that it would be good to catch other errors and exceptions too – and thankfully in ASP.net Core all of it is really easy.

Simplest solution

If you create a new project in Visual Studio – an empty ASP.NET Core Web Application, it will come with two significant files: Program.cs and Startup.cs. In Startup, you will find the following code:

public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
        }
 
        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
 
            app.Run(async (context) =>
            {
                await context.Response.WriteAsync("Hello World!");
            });
        }
    }

What’s important for us here is the if (env.IsDevelopment()) block. Your application can run in different environments: Development, Staging and Production. From official documentation:

ASP.NET Core reads the environment variable ASPNETCORE_ENVIRONMENT at application startup and stores that value in IHostingEnvironment.EnvironmentNameASPNETCORE_ENVIRONMENT can be set to any value, but three values are supported by the framework: DevelopmentStaging, and Production. If ASPNETCORE_ENVIRONMENT isn’t set, it will default to Production.

This is what gets checked in our if block. In general, Development environment is used by developers – that’s why the DeveloperExceptionPage is used. It shows all the relevant details that developers may need to resolve the issue, but can also contain some sensitive information that you don’t want to share with end users. That’s why in Production environment, we want to use some other exception page. This can be most quickly done by changing our if block to:

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseStatusCodePages();
            }

The StatusCodePages are just simple short messages stating the error and its code: Status Code: 404; Not Found. This is already better than a blank page, but what if in case of error 404 we want to redirect user somewhere, for example to our own user-friendly error page? And what about other errors?

Redirecting user in case of an error

Let’s talk catching exceptions first. In case something went wrong on the server side or the application threw an exception, we want to redirect user to an error page, let’s call it AppError.cshtml. To do that, we have to hook up the UseExceptionHandler middleware in such way:

if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Error/500");
}

This middleware will catch any exception that occurred and then send a request on specified route. In our case the exceptions will have status code 500 so the route is /Error/500. Now we have to configure a specific controller that will handle incoming requests. Let’s create it under the name ErrorController.cs with following code:

using System;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
 
namespace GlobalExceptionHandling.Controllers
{         
    [Route("Error")]
    public class ErrorController : Controller
    {
        [Route("500")]
        public IActionResult AppError()
        {
            // Get the details of the exception that occurred
            var exceptionFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
 
            if (exceptionFeature != null)
            {
                // Get which route the exception occurred at
                string routeWhereExceptionOccurred = exceptionFeature.Path;
 
                // Get the exception that occurred
                Exception exceptionThatOccurred = exceptionFeature.Error;

                // Write the exception path to debug output
                System.Diagnostics.Debug.WriteLine(routeWhereExceptionOccurred);
            }
            return View();
        }

Now we need to create a View that this controller will return. It can be informative as much as you want, but keep in mind that it should serve only static files so it won’t throw any other exceptions. Our previously mentioned View AppError.cshtml could look like that:

@{
    ViewBag.Title = "Error occurred";
}
 
<h1>We have a problem</h1>
 
<p>Sorry, an error occurred while executing your request. Don't forget that hubris leads to downfall.</p>

For users it will look like this:

Great. Now let’s handle 404 errors.

Redirecting to custom 404 page

We want to handle 404 errors in a way that the URL user entered in the browser doesn’t get changed. This can be done with following code, placed before any routing middleware:

app.Use(async (context, next) =>
{
    await next();
 
    if (context.Response.StatusCode == 404 && !context.Response.HasStarted)
    {
        string originalPath = context.Request.Path.Value;
        context.Items["originalPath"] = originalPath;
        context.Request.Path = "/Error/404";
        await next();
    }
});

This error will need a new controller route, so we have to add this to ErrorController.cs:

[Route("404")]
public IActionResult PageNotFound()
{
    // Perform any action before serving the View
    return View();
}

And lastly we create our PageNotFound.cshtmlView:

@{
    ViewBag.Title = "404";
}
 
<h1>404 - Page not found</h1>
 
<p>Seems like you've stepped into a wrong portal.</p>

For users, it will look like this:

To sum up

In our application custom error views didn’t have to be more informative as they are presented under a layout anyway:

Thanks to that, users can easily navigate to other areas of the website. Another way to approach this could be redirecting a user to a Home page straight away, but we wanted to avoid that. Keep in mind there are also other ways of redirecting, such as UseStatusCodePagesWithRedirects or UseStatusCodePagesWithReExecute, and as always you can find them in official documentation.

Leave a Reply

Your email address will not be published. Required fields are marked *