George Kosmidis

Microsoft MVP | Speaks of Azure, AI & .NET | Founder of Munich .NET
Building tomorrow @
slalom
slalom

Error handling in ASP.NET Core Web API

by George Kosmidis / Published 5 years and 5 months ago, modified 5 years and 3 months ago

We all know the traditional try-catch blocks and -used correctly- there is of course nothing wrong with them! But even though all is good with that, ASP.NET Core has an even better way of doing things, two ways actually, that can make our code cleaner and easier to read! By following the “Middleware” approach, we extract all our custom exception handling code from within the actions and centralizing it in one place thus, cleaner code!

In this post we will explore these three cases, and starting from the simple try-catch block we will refactor towards a custom error handling middleware.

Try-Catch Block

Let’s assume the following action block:

public IActionResult Get()
{
    try
    {
        var products = _data.GetProducts();
        return Ok(products);
    }
    catch (Exception ex)
    {
        //Logging logic goes here
        return StatusCode(500, "Internal server error");
    }
}

One thing we can notice here, is that the only reason we have the try-catch block is the logging logic: we need to log the exception. That means that if we had a centralized logging system that captures all exceptions, the whole action would be just two lines of perfectly readable code!

A second thing we can easily spot, is that even during development the exception is not thrown for the developer to debug. And we developers love exceptions! We see one, we know where to search for the problem!

In a more general quote, hiding exceptions has a huge impact on how maintainable a project is. If needed, catch only the specific exception type, all other cases fail-fast not fail-safe!

A solution of course, would be to just add a condition and re-throw if we are in a development environment, but imagine the amount of repeated code we would have in all our actions…!

Build-In Middleware

There is a build in middleware that we can add in the pipeline and configure it to do all the exception handling work for us. This middleware will catch exceptions, log them, and re-execute the request in an alternate pipeline (note that the request will not be re-executed if the response has already started.)
Let’s check it:

public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
    //... 
    app.UseExceptionHandler(appError =>
    {
        appError.Run(async context =>
        {
            context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
            context.Response.ContentType = "application/json";

            var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
            if(contextFeature != null)
            { 
                //Logging logic goes here
                await context.Response.WriteAsync(context.Response.StatusCode + " Internal Server Error.");
            }
        });
    });
//...

This solution works great actually! We could even tidy up things a little bit by creating an extension method for IApplicationBuilder and migrate all the code to a different file. For full control though, we need to create a custom exception middleware, that can help us build a much more sophisticated logging system.

Custom Middleware

And here we are, last but not least, the “custom middleware” approach! Check for example the code below of the simplest possible middleware:

A detailed guide on how to handle exceptions with a middleware can be found here: /handling-serializing-and-returning-exceptions-with-a-middleware-in-asp-net-core-api/

/

public class ErrorHandlingMiddleware
{
    private readonly RequestDelegate _next;
 
    public ErrorHandlingMiddleware(RequestDelegate next)
    {
        _next = next;
    }
 
    public async Task InvokeAsync(HttpContext httpContext)
    {
        try
        {
            await _next(httpContext);
        }
        catch (Exception ex)
        {
            //Logging logic goes here
            await HandleExceptionAsync(httpContext, ex);
        }
    }
 
    private static Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
 
        await context.Response.WriteAsync(context.Response.StatusCode + " Internal Server Error.");
    }
}

All we need to do now, is add it in the pipeline and it will capture all unexpected exceptions in our code. And although there are many things we can add (things that you could read in details here), the most important is to make sure we will not expose sensitive information to the end user by returning exception messages. The safest way to achieve this, is by not returning anything at all, just log the details but I guess the best of both worlds would be to check if we are in a development environment and return the actual message instead of “Internal Server Error“. Check for example the HandleExceptionAsync:

    private static Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

        var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
        var isDevelopment = environment == Microsoft.AspNetCore.Hosting.EnvironmentName.Development;
        
        if( isDevelopment )
            await context.Response.WriteAsync(exception.Message);
        else
            await context.Response.WriteAsync(context.Response.StatusCode + " Internal Server Error.");
    }
}

Conclusion

There are many more things to do with our custom middleware! For example we could loop through inner exceptions and collect all messages, serialize stack trace or even capture only specific exceptions. In any case if you are interested in a more detailed approach on a custom error handling middleware, just read my blog post on how to Handle, serialize and return exceptions with a middleware in an ASP.NET Core API

This page is open source. Noticed a typo? Or something unclear?
Edit Page Create Issue Discuss
Microsoft MVP - George Kosmidis
Azure Architecture Icons - SVGs, PNGs and draw.io libraries