The new way of Error Handling in .Net 8

Bugra Sitemkar
5 min readJul 25, 2024

--

Photo by Etienne Girardet on Unsplash

We all strive for smooth-running applications, and as you might recall, I previously discussed strategies to minimize exceptions altogether. However, even the most robust code encounters bumps in the road. That’s where exception handling comes in.

ASP.NET Core offers various ways to manage these unexpected situations. The question is, which approach is best? Let’s explore your options and find the perfect fit for your application.

Exception Handling in ASP.NET Core

The preferred approach for implementing exception handling in ASP.NET Core utilizes middleware. Middleware acts as an intermediary, allowing you to intercept and inject custom logic before or after HTTP requests are processed. This flexibility makes it ideal for centralizing exception handling.

You can achieve this by incorporating a try-catch block within your middleware component. If an exception occurs during request processing, the catch block will capture it. Here, you can choose to log the error for further analysis and then return an appropriate HTTP error response to the client.

ASP.NET Core offers three primary methods for creating middleware:

  1. Request Delegates: This approach involves defining a function that inputs an object. This function represents the core logic of your middleware and can handle exceptions within its scope.
  2. By Convention: This method leverages a convention-based approach. You establish a InvokeAsync method within your middleware class, responsible for processing the request and handling exceptions as needed.
  3. IMiddleware Interface: This approach offers greater control when implementing the IMiddleware interface. This interface dictates the required methods, which becomes the entry point for your middleware logic and exception handling.

The convention-based approach requires you to define an InvokeAsync method.

Here’s an example of ExceptionHandleMiddleware defined by convention:

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

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

public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception exception)
{
_logger.LogError(
exception, "Exception: {Message}", exception.Message);

var problemDetails = new ProblemDetails
{
Status = StatusCodes.Status500InternalServerError,
Title = "Error"
};

context.Response.StatusCode =
StatusCodes.Status500InternalServerError;

await context.Response.WriteAsJsonAsync(problemDetails);
}
}
}

The ExceptionHandleMiddleware will catch any unhandled exception and return a Problem Details response. You can decide how much information you want to return to. You also need to add this middleware to the ASP.NET Core request pipeline:

app.UseMiddleware<ExceptionHandleMiddleware>();

Streamlined Exception Handling with IExceptionHandler

ASP.NET Core 8 brings a cleaner approach to exception management with the introduction of the IExceptionHandler interface. This interface acts as the central point for handling exceptions within your application.

The built-in exception handler middleware relies on implementations of IExceptionHandler to deal with errors that occur during processing. This interface provides a single method: TryHandleAsync.

The TryHandleAsync method takes the reins when an exception arises. Its role is to determine if it can handle the specific exception within the ASP.NET Core pipeline. If it can effectively address the issue, it returns true. However, if the exception is beyond its capabilities, it returns false. This allows you to create custom logic for handling different types of exceptions, providing a more granular and adaptable approach to error management.

Here’s an example ofGlobalExceptionHandle implementation:

internal sealed class GlobalExceptionHandle : IExceptionHandler
{
private readonly ILogger<GlobalExceptionHandle> _logger;

public GlobalExceptionHandle(ILogger<GlobalExceptionHandle> logger)
{
_logger = logger;
}

public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
_logger.LogError(
exception, "Exception: {Message}", exception.Message);

var problemDetails = new ProblemDetails
{
Status = StatusCodes.Status500InternalServerError,
Title = "Error"
};

httpContext.Response.StatusCode = problemDetails.Status.Value;

await httpContext.Response
.WriteAsJsonAsync(problemDetails, cancellationToken);

return true;
}
}

Adding Custom Exception Handling

To incorporate your own IExceptionHandler implementation into the ASP.NET Core request pipeline, two steps are required:

  1. Dependency Injection: Register the IExceptionHandler service. This is typically done using the AddExceptionHandler method in your ConfigureServices method. The service is usually registered with a singleton lifetime, so be mindful of injecting services with different lifetimes that might cause conflicts.
  2. Middleware Registration: Include the ExceptionHandlerMiddleware within the request pipeline. This middleware is responsible for utilizing the registered IExceptionHandler implementations to handle exceptions. You'll typically use the app.UseExceptionHandler method in your Configure method to achieve this.

Additional Considerations: You might also encounter the use of AddProblemDetails alongside this process. This method helps generate formatted "Problem Details" responses for common exceptions, improving the structure and clarity of error messages returned to the client.

builder.Services.AddExceptionHandler<GlobalExceptionHandle>();
builder.Services.AddProblemDetails();

You also need to call UseExceptionHandler to add the ExceptionHandlerMiddleware to the request pipeline:

app.UseExceptionHandler();

Chaining Exception Handlers

You can stack multiple IExceptionHandler implementations to create a layered approach to error handling. These handlers are processed in the order they're registered. This flexibility allows for granular control over different types of exceptions.

For instance, you might use exceptions as a form of flow control. By defining custom exceptions like BadRequestException and NotFoundException, you can map specific error conditions to appropriate HTTP status codes. This helps in creating a more structured and predictable API response.

Here’s an example ofBadRequestExceptionHandle implementation:

internal sealed class BadRequestExceptionHandle : IExceptionHandler
{
private readonly ILogger<BadRequestExceptionHandle> _logger;

public BadRequestExceptionHandle(ILogger<BadRequestExceptionHandle> logger)
{
_logger = logger;
}

public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
if (exception is not BadRequestException badRequestException)
{
return false;
}

_logger.LogError(
badRequestException,
"Exception: {Message}",
badRequestException.Message);

var problemDetails = new ProblemDetails
{
Status = StatusCodes.Status400BadRequest,
Title = "BadRequest",
Detail = badRequestException.Message
};

httpContext.Response.StatusCode = problemDetails.Status.Value;

await httpContext.Response
.WriteAsJsonAsync(problemDetails, cancellationToken);

return true;
}
}

And here's an example ofNotFoundExceptionHandle implementation:

internal sealed class NotFoundExceptionHandle : IExceptionHandler
{
private readonly ILogger<NotFoundExceptionHandle> _logger;

public NotFoundExceptionHandle(ILogger<NotFoundExceptionHandle> logger)
{
_logger = logger;
}

public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
if (exception is not NotFoundException notFoundException)
{
return false;
}

_logger.LogError(
notFoundException,
"Exception: {Message}",
notFoundException.Message);

var problemDetails = new ProblemDetails
{
Status = StatusCodes.Status404NotFound,
Title = "NotFound",
Detail = notFoundException.Message
};

httpContext.Response.StatusCode = problemDetails.Status.Value;

await httpContext.Response
.WriteAsJsonAsync(problemDetails, cancellationToken);

return true;
}
}

You also need to register both exception handlers by calling AddExceptionHandler:

builder.Services.AddExceptionHandler<BadRequestExceptionHandle>();
builder.Services.AddExceptionHandler<NotFoundExceptionHandle>();

The BadRequestExceptionHandle will execute first and try to handle the exception. If the exception isn't handled, NotFoundExceptionHandle will execute next and attempt to handle the exception.

Shifting Gears in Exception Handling

While middleware has served ASP.NET Core well for exception handling, the introduction of the IExceptionHandler interface offers a more streamlined and flexible approach. This improved system allows for centralized management and custom handling logic, making error management in your ASP.NET Core 8 projects more efficient and adaptable.

A Note on Flow Control:

It’s important to remember that exceptions are generally intended for unexpected errors that disrupt normal program flow. While you can technically use them for flow control, the Result pattern is often a more appropriate and maintainable choice.

--

--

Bugra Sitemkar

Software Engineer | .NET Enthusiast | Writer. Diving deep into software craftsmanship. 🚀