I want to implement global exception handling in my .NET Core API. What are the best practices for using middleware or filters for consistent error responses? Should I log errors at the middleware level or elsewhere?
What's the Best Way to Handle Global Exception Handling in .NET Core?
1 Answer
Global exception handling in .NET Core / ASP.NET Core is a centralized mechanism for catching unhandled exceptions, logging them, and returning consistent error responses to clients. Instead of wrapping every controller action in
try-catch blocks, you configure a single global exception handler that processes all unexpected errors.
Why Use Global Exception Handling?
Implementing global exception handling provides several advantages:
- Reduces repetitive
try-catchcode. - Keeps controllers and services clean.
- Returns consistent API responses.
- Logs errors in one place.
- Prevents sensitive exception details from being exposed.
- Improves application maintainability.
Option 1: Use Built-in Exception Handler Middleware (Recommended)
ASP.NET Core provides built-in middleware for handling unhandled exceptions.
Program.cs (.NET 6+)
// Add middleware before routing
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/error");
}
else
{
app.UseDeveloperExceptionPage();
}
Create an Error Controller:
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
[ApiController]
public class ErrorController : ControllerBase
{
[Route("/error")]
public IActionResult HandleError()
{
// Get exception details
var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
// Log exception here if needed
return Problem(
title: "An unexpected error occurred.",
statusCode: 500
);
}
}
Advantages
- Built into ASP.NET Core
- Easy to configure
- Works well for MVC and Web APIs
- Supports Problem Details (RFC 7807)
Option 2: Custom Exception Middleware (Most Flexible)
For enterprise applications, creating custom middleware offers greater control.
Custom Middleware
using System.Net;
using System.Text.Json;
public class ExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionMiddleware> _logger;
// Constructor
public ExceptionMiddleware(RequestDelegate next,
ILogger<ExceptionMiddleware> logger)
{
_next = next;
_logger = logger;
}
// Middleware execution
public async Task Invoke(HttpContext context)
{
try
{
// Continue request pipeline
await _next(context);
}
catch (Exception ex)
{
// Log exception
_logger.LogError(ex, ex.Message);
// Set response
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
context.Response.ContentType = "application/json";
var response = new
{
Success = false,
Message = "Something went wrong.",
StatusCode = 500
};
await context.Response.WriteAsync(
JsonSerializer.Serialize(response));
}
}
}
Register Middleware
app.UseMiddleware<ExceptionMiddleware>();
Response
{
"success": false,
"message": "Something went wrong.",
"statusCode": 500
}
Option 3: Exception Filters
Exception filters work only within the MVC pipeline.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
public class GlobalExceptionFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
context.Result = new ObjectResult(new
{
Message = "Server Error"
})
{
StatusCode = 500
};
context.ExceptionHandled = true;
}
}
Register:
builder.Services.AddControllers(options =>
{
options.Filters.Add<GlobalExceptionFilter>();
});
Returning Different Status Codes
It's a good practice to map exception types to appropriate HTTP status codes.
switch (exception)
{
case ArgumentException:
statusCode = StatusCodes.Status400BadRequest;
break;
case UnauthorizedAccessException:
statusCode = StatusCodes.Status401Unauthorized;
break;
case KeyNotFoundException:
statusCode = StatusCodes.Status404NotFound;
break;
default:
statusCode = StatusCodes.Status500InternalServerError;
break;
}
Example responses:
400 Bad Request
{
"message": "Invalid input."
}
404 Not Found
{
"message": "Resource not found."
}
500 Internal Server Error
{
"message": "Unexpected server error."
}
Logging Exceptions
Use the built-in logging framework rather than writing logs manually.
_logger.LogError(exception,
"Error occurred while processing request.");
You can integrate logging providers such as:
- Console Logger
- Debug Logger
- Event Log
- Azure Application Insights
- Serilog
- NLog
Use ProblemDetails for APIs
ASP.NET Core supports RFC 7807 Problem Details, which provides a standard error format.
return Results.Problem(
title: "Unexpected Error",
detail: exception.Message,
statusCode: 500
);
Example:
{
"type": "about:blank",
"title": "Unexpected Error",
"status": 500,
"detail": "Database connection failed."
}
In production, avoid returning internal exception messages to clients; log the detailed exception and return a generic
detail instead.
Best Practices
- Use centralized exception handling instead of repetitive
try-catchblocks. - Prefer middleware for application-wide exception handling.
- Return appropriate HTTP status codes for different exception types.
- Log full exception details, including stack traces, to a logging provider.
- Avoid exposing sensitive exception information in production responses.
- Use the standard
ProblemDetailsformat for REST APIs. - Handle expected business or validation errors separately from unexpected exceptions.
- Place exception handling middleware early in the request pipeline so it can catch downstream exceptions.
Which Approach Should You Choose?
| Approach | Best For | Pros | Cons |
|---|---|---|---|
| Built-in Exception Handler | Most applications | Simple, reliable, integrates with ASP.NET Core | Less customizable |
| Custom Middleware | Enterprise APIs | Full control over logging and responses | More code to maintain |
| Exception Filters | MVC-specific scenarios | Easy for controller logic | Doesn't catch exceptions outside the MVC pipeline |
Conclusion
For most modern ASP.NET Core applications, global exception handling through middleware is the preferred solution. It centralizes error handling, produces consistent API responses, integrates cleanly with logging, and keeps business logic free from repetitive error-handling code. The built-in exception handler is sufficient for many applications, while a custom middleware is an excellent choice when you need custom error formats, exception mapping, or advanced logging behavior.