Global Exception Handling in .NET Core API

The exception-handling features in .NET help us in handling abrupt errors that could appear in the code.

Though we can use try-catch blocks to handle errors at the method/code level we can add the try-catch blocks at the global level which makes the code readable and easily maintainable.

In this blog, we will see a few common ways of global exception-handling techniques.

  1. Error endpoint

  2. Built-in middleware

  3. Custom middleware

  4. Filter attribute

1) Error endpoint

We can use UseExceptionHandler which adds a middleware to the pipeline that will catch the exceptions, logs them, and re-execute the request in an alternate pipeline. The request will not be re-executed if the response has already started.

In program.cs, call the UseExceptionHandler to add the exception-handling middleware.

var app = builder.Build();
{
    // Configure the HTTP request pipeline.
    if (app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/error-development");
    }
    else
    {
        app.UseExceptionHandler("/error");
    }
    app.UseHttpsRedirection();
    app.UseAuthorization();
    app.MapControllers();
    app.Run();
}

And then add a controller action to respond to the "/error" route.

public class ErrorController : ControllerBase
{
    [Route("/error")]
    public IActionResult Error()
    {
        var exception = HttpContext.Features.
                        Get<IExceptionHandlerFeature>()?.Error;
        return Problem(title: exception?.Message, statusCode: 500);
    }
}

2) Built-in middleware

We can use the same UseExceptionHandler middleware with lambda to handle the exceptions in the application. Using lambda allows access to the error before returning the response.

Let's look into the code which uses lambda for exception handling.

app.UseExceptionHandler(app =>
{
    app.Run(async context =>
    {
      context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
      context.Response.ContentType = "application/json";
      var exceptionHandlerFeature = context.Features.Get<IExceptionHandlerFeature>();
      if (exceptionHandlerFeature != null)
      {
          _logger.LogError($"Something went wrong:{exceptionHandlerFeature.Error}");
          await context.Response.WriteAsync(JsonSerializer.Serialize(new
          {
              StatusCode = context.Response.StatusCode,
              Message = "Internal Server Error."
          }));
       }
    });
});

3) Custom middleware

Let's create a new class ErrorHandlingMiddleware.cs and inject the logger and the RequestDelegate in the constructor. Then add a method InvokeAsync which accepts the HttpContext as a parameter. The RequestDelegate and InvokeAsync are required to process the HTTP request.

Any requests that are not processed successfully and throw abrupt errors will be caught in the catch block where we log them using any logging mechanism.

Also, we can send back very minimal details with a generic message in the response not to expose any sensitive information.

Note: We need to register the custom middleware at the top of the program.cs file as shown below.

ErrorHandlingMiddleware.cs

public class ErrorHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILoggerService _logger;

    public ErrorHandlingMiddleware(RequestDelegate next, ILoggerService logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        try
        {
            await _next(httpContext);
        }
        //handle specific exceptions if required
        catch (Exception ex)
        {
            _logger.LogError($"Something went wrong: {ex}");
            await HandleExceptionAsync(httpContext);
        }
    }

    private static Task HandleExceptionAsync(HttpContext httpContext)
    {
    var result = JsonSerializer.Serialize(new { error = "An error occurred while processing your request." });
    httpContext.Response.ContentType = "application/json";
    httpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
    return httpContext.Response.WriteAsync(result);
    }
}

Register the custom middleware in Program.cs

var app = builder.Build();
{
    app.UseMiddleware<ErrorHandlingMiddleware>();
    app.UseHttpsRedirection();
    app.UseAuthorization();
    app.MapControllers();
    app.Run();
}

4) Filter Attribute

We can also use exception filters to handle the exceptions. We have the ExceptionFilterAttribute class which is an abstract filter that runs asynchronously after a request throws an exception. The subclass which implements the ExceptionFilterAttribute class should override OnExceptionAsync or OnException method.

Let's create a class and implement the ExceptionFilterAttribute class and override the OnException and handle the exception in it.

public class ErrorHandlingFilteAttribute : ExceptionFilterAttribute
{
    public override void OnException(ExceptionContext context)
    {
        var exception = context.Exception; //log here
        var problemDetails = new ProblemDetails()
        {
            Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1",
            Title = "A error occurred while processing your request.",
            Status = (int)HttpStatusCode.InternalServerError
        };

        context.Result = new ObjectResult(problemDetails)
        {
            StatusCode = 500
        };
        context.ExceptionHandled = true;
    }
}

Did you find this article valuable?

Support .NET - Sankarshan Ramesh by becoming a sponsor. Any amount is appreciated!