Exception handling is a critical aspect of building robust, secure, and maintainable Web APIs. In ASP.NET Core, handling exceptions gracefully ensures that the API provides meaningful error information to consumers without exposing sensitive internal details or causing the application to crash.
In this article, we explore best practices for exception handling in ASP.NET Core Web APIs, including practical examples and architectural recommendations.
Why Exception Handling Matters
Improper exception handling can lead to:
- Application crashes
- Exposure of sensitive information
- Unclear error messages to clients
- Difficulties in debugging and monitoring
By implementing structured and centralized exception handling, you improve resilience, security, and developer experience.
1. Use Middleware for Global Exception Handling
Why?
Global exception handling centralizes error logic and ensures consistency across endpoints.
How?
Create a custom middleware:
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception");
context.Response.ContentType = "application/json";
context.Response.StatusCode = ex switch
{
ArgumentException => StatusCodes.Status400BadRequest,
KeyNotFoundException => StatusCodes.Status404NotFound,
_ => StatusCodes.Status500InternalServerError
};
var result = JsonSerializer.Serialize(new
{
error = ex.Message,
statusCode = context.Response.StatusCode
});
await context.Response.WriteAsync(result);
}
}
}
Register it in Startup.cs
or Program.cs
:
app.UseMiddleware<ExceptionHandlingMiddleware>();
2. Avoid Try-Catch in Every Action
Instead of repeating try-catch blocks in controllers, let the middleware handle exceptions globally. Reserve local try-catch blocks for cases where:
- You want to retry logic (e.g., for transient errors)
- You need to handle exceptions with specific logic before propagating
- You’re consuming third-party APIs where fine-grained control is needed
3. Use ProblemDetails for Standardized Error Responses
ASP.NET Core supports RFC 7807 for standardized error responses.
4. Create Custom Exception Types
Define domain-specific exceptions to distinguish between different failure types:
public class NotFoundException : Exception
{
public NotFoundException(string message) : base(message) { }
}
public class ValidationException : Exception
{
public ValidationException(string message) : base(message) { }
}
Handle them specifically in your middleware:
context.Response.StatusCode = ex switch
{
NotFoundException => 404,
ValidationException => 400,
_ => 500
};
5. Use Filters for Cross-Cutting Concerns
ASP.NET Core provides IExceptionFilter
and IAsyncExceptionFilter
for exception handling at the controller level:
public class ApiExceptionFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
var response = new
{
error = context.Exception.Message,
stackTrace = context.Exception.StackTrace
};
context.Result = new JsonResult(response)
{
StatusCode = 500
};
}
}
Register globally:
services.AddControllers(options =>
{
options.Filters.Add<ApiExceptionFilter>();
});
6. Logging Is Critical
Always log exceptions, especially those leading to HTTP 5xx errors:
- Use structured logging (e.g., with Serilog)
- Log correlation IDs for tracing
- Avoid logging stack traces in production response bodies
7. Don't Leak Internal Details
Never return raw exception messages or stack traces to clients, especially in production. Instead:
- Return generic messages
- Log internal details privately
- Use HTTP status codes appropriately
8. Return Appropriate Status Codes
Follow HTTP semantics:
Exception Type |
HTTP Status Code |
ValidationException |
400 Bad Request |
NotFoundException |
404 Not Found |
UnauthorizedAccessException |
401 Unauthorized |
ForbiddenAccessException |
403 Forbidden |
GeneralException |
500 Internal Server Error |
Use exception mapping to align exceptions with the correct status codes.
9. Handle Model Validation Failures
Use [ApiController]
attribute to auto-handle model state errors:
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpPost]
public IActionResult Create(ProductModel model)
{
// ModelState is automatically validated
return Ok();
}
}
Customize responses with InvalidModelStateResponseFactory
if needed.
10. Use Built-in Middleware for Development
Enable detailed errors during development:
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
Summary
Effective exception handling in ASP.NET Core Web APIs involves:
- ✅ Centralized middleware for global error handling
- ✅ Custom exception types for clarity
- ✅ Use of
ProblemDetails
for standard responses
- ✅ Structured logging for diagnostics
- ✅ Avoiding information leakage in production