Implementing global error handling in .Net 6

In this quick tutorial, I will be going over a simple way to apply global error handling to an API created in .Net 6. The goal of this article is to eliminate the need to have try/catch blocks within each controller method while still handling errors effectively. To do this, we will create a dedicated middleware layer to handle the errors, so our controller code doesn’t have to.

Setup

To get started, I will be working with an example .Net 6 webAPI project. We will have one controller containing a few simple GET requests which each throw a different error. For demonstrative purposes, we will start with the controller handling errors and then update the project to include the new middleware. To test that everything is working as expected, I will be using swagger to make calls to the API.

Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
//// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddTransient<IErrorService, ErrorService>();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.MapControllers();
app.Run();
ErrorController.cs

In the controller code, we have 3 endpoints. Each endpoint calls our ErrorService in order to throw an error.

    [ApiController]
    [Route("api/error")]
    public class ErrorController : ControllerBase
    {
        private readonly IErrorService errorService;

        public ErrorController(IErrorService errorService)
        {
            this.errorService = errorService;
        }

        [HttpGet("internalServerError")]
        public ActionResult GetServerError()
        {
            try
            {
                errorService.NullReference();
                return Ok();
            }
            catch(Exception ex)
            {
                return StatusCode(500, ex.Message);
            }
        }

        [HttpGet("notFound")]
        public ActionResult GetNotFound()
        {
            try
            {
                errorService.NotFound();
                return Ok();
            }
            catch(Exception ex)
            {
                return NotFound(ex.Message);
            }

        }

        [HttpGet("badRequest")]
        public ActionResult GetBadRequest()
        {
            try
            {
                errorService.BadRequest();
                return Ok();
            }
            catch(Exception ex)
            {
                return BadRequest(ex.Message);
            }

        }
    }
ErrorService.cs

The ErrorService methods throw either a NullReferenceException, KeyNotFoundException, or BadRequestException.

public class ErrorService : IErrorService
{
    public void NullReference()
    {
        throw new NullReferenceException();
    }

    public void NotFound()
    {
        throw new KeyNotFoundException();
    }

    public void BadRequest()
    {
        throw new BadHttpRequestException("Invalid data");
    }
}


IErrorService.cs
{
    public interface IErrorService
    {
        void NullReference();

        void NotFound();

        void BadRequest();
    }
}

Testing our endpoints with Swagger

Now that we have our basic application setup, lets run it and make sure that everything is working properly. To do this, I will start the application and make a GET request to each endpoint via the Swagger UI.

500 Error
404 Error
400 Error
So far, everything is working as we would expect it to. But we are stuck in a situation where any time we want to add a new request to our API, we will need to write the same try/catch block and handle our errors in each situation. Let’s make our job easier in the future by adding a global exception middleware to the application and consolidate our error handling code to be all in one place!

Upgrading our API

To get rid of the need for catching errors in our controller code, we will create our ExceptionMiddleware class and add it to our Program.cs file.

ExceptionMiddleware.cs

This new class will contain all our error handling code. We can add cases to deal with each error as needed and return the appropriate error code. We can also grab the error message off the Exception being thrown and serialize it to be written to the response body. In this example, that message will be added to the response JSON as property “message”.

    public class ExceptionMiddleware
    {
        private readonly RequestDelegate next;
        private readonly ILogger<ExceptionMiddleware> logger;

        public ExceptionMiddleware(
            RequestDelegate next, 
            ILogger<ExceptionMiddleware> logger)
        {
            this.next = next;
            this.logger = logger;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            try
            {
                await next(context);
            }
            catch (Exception ex)
            {
                logger.LogError(ex, ex.Message);

                switch (ex)
                {
                    case KeyNotFoundException:
                        context.Response.StatusCode = (int)HttpStatusCode.NotFound;
                        break;
                    case BadHttpRequestException:
                        context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
                        break;
                    default:
                        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

                        break;
                }
                context.Response.ContentType = "application/json";

                var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };

                var json = JsonSerializer.Serialize(new { message = ex?.Message}, options);

                await context.Response.WriteAsync(json);
            }
        }
    }
Program.cs

To plug in our new ExceptionMiddleware, we need to add it to the app pipeline. It is important to insert exception handling early in the pipeline to ensure that all errors are caught.

To learn more about middleware and how the pipeline works, visit the Microsoft Docs

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
//// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddTransient<IErrorService, ErrorService>();

var app = builder.Build();

//// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

// Exception handler
app.UseMiddleware<ExceptionMiddleware>();

app.MapControllers();
app.Run();
ErrorController.cs

Now that we have our middleware set up, we can remove the try/catch blocks from our controller code.

    [ApiController]
    [Route("api/error")]
    public class ErrorController : ControllerBase
    {
        private readonly IErrorService errorService;

        public ErrorController(IErrorService errorService)
        {
            this.errorService = errorService;
        }

        [HttpGet("internalServerError")]
        public ActionResult GetServerError()
        {
            errorService.NullReference();
            return Ok();
        }

        [HttpGet("notFound")]
        public ActionResult GetNotFound()
        {
            errorService.NotFound();
            return Ok();
        }

        [HttpGet("badRequest")]
        public ActionResult GetBadRequest()
        {
            errorService.BadRequest();
            return Ok();
        }
    }
That looks much better. Let’s start the app again and see what our error responses look like.

500 Error
404 Error
400 Error

Conclusion

Success! We were able to consolidate our error handling, serialize our error messages to be included in the response, and clean up our controller code so it is much more concise.

About Intertech

Intertech is a Software Development Consulting Firm that provides single and multiple turnkey software development teams, available on your schedule and configured to achieve success as defined by your requirements independently or in co-development with your team. Intertech teams combine proven full-stack, DevOps, Agile-experienced lead consultants with Delivery Management, User Experience, Software Development, and QA experts in Business Process Automation (BPA), Microservices, Client- and Server-Side Web Frameworks of multiple technologies, Custom Portal and Dashboard development, Cloud Integration and Migration (Azure and AWS), and so much more. Each Intertech employee leads with the soft skills necessary to explain complex concepts to stakeholders and team members alike and makes your business more efficient, your data more valuable, and your team better. In addition, Intertech is a trusted partner of more than 4000 satisfied customers and has a 99.70% “would recommend” rating.