Web API  

Best Practices for Exception Handling in ASP.NET Core Web APIs

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