Building a Global Error Handling Middleware in .NET Web API

Photo by NASA on Unsplash

Building a Global Error Handling Middleware in .NET Web API

Error handling is a critical aspect of web application development. Whether it's handling unexpected exceptions, validating user input, or dealing with various types of errors gracefully, a well-structured error handling mechanism is essential. In .NET, you can create a global error handling middleware to capture and manage errors that occur during the request pipeline. In this article, we'll walk you through the process of building a robust global error handling middleware in .NET.

What is Middleware in .NET ?

Middleware is a fundamental concept in ASP.NET Core that enables you to build a pipeline of components that process HTTP requests. Each middleware component in the pipeline can perform specific tasks, such as authentication, logging, routing, and error handling. The middleware components are executed in the order they are added to the pipeline, allowing you to create a flexible and modular request processing flow.

The Need for Global Error Handling Middleware

Errors can occur at various stages during the processing of an HTTP request, such as during routing, model binding, or while executing application logic. Handling these errors consistently and gracefully is essential for providing a good user experience and ensuring the stability of your application.

A global error handling middleware helps you:

  1. Capture and log exceptions that occur during request processing.

  2. Provide a consistent error response format to clients.

  3. Customize error responses based on the type of error or client request.

  4. Ensure that errors don't crash the application but are handled gracefully.

Structure of a middleware class

When you define a middleware component in .NET, it typically follows this pattern:

  1. It takes a RequestDelegate as a constructor parameter. This RequestDelegate represents the next middleware component in the pipeline.

  2. In its InvokeAsync method, it invokes the next middleware component using the await _next(context); line. This line effectively hands off the request to the next middleware in the pipeline for further processing.

  3. After the next middleware has finished processing the request and generating a response, control returns to the current middleware, allowing you to perform additional tasks, such as logging, error handling, or modifying the response. Note that you can add code before as well as after the await _next(context) to execute code either before or after the next middleware component.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using System;
using System.Threading.Tasks;

public class MyMiddleware
{
    private readonly RequestDelegate _next;

    public MyMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // Code to be executed before calling the next middleware.
        Console.WriteLine("Before MyMiddleware");

        // Call the next middleware in the pipeline.
        await _next(context);

        // Code to be executed after the next middleware has completed.
        Console.WriteLine("After MyMiddleware");
    }
}

The next step is to register this middleware, usually in the Startup.cs class.

public void Configure(IApplicationBuilder app)
{
    // ...

    // Register your middleware in the pipeline.
    app.UseMiddleware<MyMiddleware>();

    // ...

    app.UseRouting();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

Building the Global Error Handling Middleware

To create a global error-handling middleware in .NET, follow these steps:

1. Create a Middleware Class

First, create a class for your error-handling middleware. This class should have a constructor that accepts a RequestDelegate and any additional dependencies, such as a logger. Here's an example:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

public class ErrorHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ErrorHandlingMiddleware> _logger;

    public ErrorHandlingMiddleware(RequestDelegate next, ILogger<ErrorHandlingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            // Call the next middleware in the pipeline.
            await _next(context);
        }
        catch (Exception ex)
        {
            // Handle the exception and log it.
            _logger.LogError($"An error occurred: {ex.Message}");

            // Optionally, you can customize the response sent to the client.
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            context.Response.ContentType = "text/plain";
            await context.Response.WriteAsync("Internal Server Error");

            // You can also redirect to an error page or perform other actions as needed.
        }
    }
}

In the above code, we have encapsulated the line await _next(context) within a try-catch block, which means we can catch and handle any kind of exception that bubbles up from the next pipeline component. We capture exceptions, log them using the provided logger, and customize the response sent to the client. You can tailor the error-handling logic to your specific needs.

2. Register the Middleware

Next, you need to register your error-handling middleware in the Startup.cs class's Configure method. This is where you specify the order in which your middleware should run in the pipeline. Typically, error-handling middleware should be one of the first components in the pipeline, before any other middleware components that might throw exceptions.

Here's how you register the middleware:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        // Use your custom error handling middleware in production.
        app.UseMiddleware<ErrorHandlingMiddleware>();
        app.UseHsts();
    }

    // ... other middleware and configurations ...

    app.UseRouting();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

In the code above, we check whether the application is running in development mode and use the built-in developer exception page in that case. In production, we add our custom error handling middleware using app.UseMiddleware<ErrorHandlingMiddleware>(). Ensure that your error handling middleware comes before other middleware to catch exceptions early in the pipeline.

Customizing the Global Error Handling Middleware

You can further customize the error-handling logic within the InvokeAsync method of your middleware. For example, you can redirect to a custom error page, send different error responses based on the exception type, or include additional information in the error response.

Here's an example of customizing the error response based on the exception type:

public async Task InvokeAsync(HttpContext context)
{
    try
    {
        // Call the next middleware in the pipeline.
        await _next(context);
    }
    catch (NotFoundException ex)
    {
        // Handle the specific exception and log it.
        _logger.LogWarning($"Resource not found: {ex.Message}");

        // Customize the response for a 404 Not Found error.
        context.Response.StatusCode = StatusCodes.Status404NotFound;
        context.Response.ContentType = "text/plain";
        await context.Response.WriteAsync("Resource not found");
    }
    catch (Exception ex)
    {
        // Handle other exceptions and log them.
        _logger.LogError($"An error occurred: {ex.Message}");

        // Customize the response for other types of errors.
        context.Response.StatusCode = StatusCodes.Status500InternalServerError;
        context.Response.ContentType = "text/plain";
        await context.Response.WriteAsync("Internal Server Error");
    }
}

In this example, we catch a specific NotFoundException and return a 404 Not Found response for that particular exception type. For all other exceptions, we return a generic 500 Internal Server Error response.

Conclusion

Implementing a global error-handling middleware in ASP.NET Core is crucial for building robust and reliable web applications. It allows you to capture and manage exceptions gracefully, ensuring a better user experience and making it easier to diagnose and resolve issues in production.

By following the steps outlined in this article, you can create a custom error handling middleware tailored