Implementing Global Error Handling via Middleware in .NET
In modern web applications, error handling is crucial to provide a robust and user-friendly experience. ASP.NET Core offers a powerful way to handle errors globally using middleware. This article will guide you through the implementation of a global error handler via middleware, making your controllers cleaner and your application more maintainable.
Introduction
Handling errors in a web application can become complex as your codebase grows. Traditionally, you might have scattered try-catch blocks across your controllers, leading to repetitive and cluttered code. A global error handler middleware can centralize error handling, improve readability, and maintain consistency throughout your application.
Global Error Handling Middleware
Let’s dive into the implementation of a global error handling middleware.
public class GlobalErrorHandlingMiddleware
{
private readonly ILogger<GlobalErrorHandlingMiddleware> _logger;
private readonly RequestDelegate _next;
public GlobalErrorHandlingMiddleware(ILogger<GlobalErrorHandlingMiddleware> logger, RequestDelegate next)
{
_logger = logger;
_next = next;
}
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
await HandleExceptionAsync(context, ex);
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception exception)
{
HttpStatusCode status;
string? stackTrace = string.Empty;
string message;
Type? exceptionType = exception.GetType();
if (exceptionType == typeof(DirectoryNotFoundException) ||
exceptionType == typeof(DllNotFoundException) ||
exceptionType == typeof(EntryPointNotFoundException) ||
exceptionType == typeof(FileNotFoundException) ||
exceptionType == typeof(KeyNotFoundException))
{
message = exception.Message;
status = HttpStatusCode.NotFound;
stackTrace = exception.StackTrace;
}
else if (exceptionType == typeof(NotImplementedException))
{
status = HttpStatusCode.NotImplemented;
message = exception.Message;
stackTrace = exception.StackTrace;
}
else if (exceptionType == typeof(UnauthorizedAccessException) ||
exceptionType == typeof(AuthenticationException))
{
status = HttpStatusCode.Unauthorized;
message = exception.Message;
stackTrace = exception.StackTrace;
}
else
{
status = HttpStatusCode.InternalServerError;
message = exception.Message;
stackTrace = exception.StackTrace;
}
string exceptionResult = System.Text.Json.JsonSerializer.Serialize(new { error = message, stackTrace });
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)status;
return context.Response.WriteAsync(exceptionResult);
}
}
Applying the Middleware
To apply this middleware, add it to the request pipeline in the `Startup.cs` file or the `Program.cs` file if you are using .NET 6 or later.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseMiddleware<GlobalErrorHandlingMiddleware>();
// Other middlewares
}
Controller Example Before and After
Let’s compare a typical controller action before and after applying global error handling middleware.
Before:
[HttpPost]
public async Task<IActionResult> ProcessData([FromBody] RequestDto requestDto, CancellationToken cancellationToken)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
try
{
var request = _mapper.Map<Request>(requestDto);
var result = await _service.ProcessAsync(request, cancellationToken);
var resultDto = _mapper.Map<ResponseDto>(result);
return Ok(resultDto);
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
return StatusCode(500, "Internal server error.");
}
}
After:
[HttpPost]
public async Task<IActionResult> ProcessData([FromBody] RequestDto requestDto, CancellationToken cancellationToken)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var request = _mapper.Map<Request>(requestDto);
var result = await _service.ProcessAsync(request, cancellationToken);
var resultDto = _mapper.Map<ResponseDto>(result);
return Ok(resultDto);
}
Summary
By implementing a global error handling middleware, you can significantly reduce the amount of repetitive error handling code in your controllers, making them cleaner and easier to maintain. This approach not only centralizes error management but also ensures a consistent response structure across your application.
With the middleware in place, your controllers focus solely on their primary responsibility — handling the business logic — while the middleware takes care of logging and formatting error responses. Adopting this pattern is a step towards building more robust and maintainable .NET applications.