The new way of Error Handling in .Net 8
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:
- 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.
- 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. - 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:
- Dependency Injection: Register the
IExceptionHandler
service. This is typically done using theAddExceptionHandler
method in yourConfigureServices
method. The service is usually registered with a singleton lifetime, so be mindful of injecting services with different lifetimes that might cause conflicts. - Middleware Registration: Include the
ExceptionHandlerMiddleware
within the request pipeline. This middleware is responsible for utilizing the registeredIExceptionHandler
implementations to handle exceptions. You'll typically use theapp.UseExceptionHandler
method in yourConfigure
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.