ValidationException => StatusCodes.Status400BadRequest,
NotFoundException => StatusCodes.Status404NotFound,
UnauthorizedException => StatusCodes.Status401Unauthorized,
ForbiddenException => StatusCodes.Status403Forbidden,
_ => StatusCodes.Status500InternalServerError
};
var problemDetails = new ProblemDetails
{
Status = statusCode,
Title = GetTitle(statusCode),
Detail = _environment.IsDevelopment() ? exception.ToString() : exception.Message,
Instance = context.Request.Path,
Extensions =
{
["traceId"] = correlationId,
["errorCode"] = GetErrorCode(exception)
}
};
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/problem+json";
// Log with structured data
_logger.LogError(exception, "Unhandled exception processed. TraceId: {TraceId}, Path: {Path}",
correlationId, context.Request.Path);
await context.Response.WriteAsJsonAsync(problemDetails);
}
private static string GetTitle(int statusCode) => statusCode switch
{
400 => "Bad Request",
401 => "Unauthorized",
403 => "Forbidden",
404 => "Not Found",
500 => "Internal Server Error",
_ => "Error"
};
private static string GetErrorCode(Exception ex) => ex switch
{
ValidationException => "VALIDATION_ERROR",
NotFoundException => "RESOURCE_NOT_FOUND",
_ => "INTERNAL_ERROR"
};
}
#### 2. Custom Exception Hierarchy
Define specific exception types for business logic errors. This allows the middleware to distinguish between expected business failures and unexpected system crashes.
```csharp
public class AppException : Exception
{
public string ErrorCode { get; }
public AppException(string message, string errorCode) : base(message) => ErrorCode = errorCode;
}
public class ValidationException : AppException
{
public ValidationException(string message) : base(message, "VALIDATION_ERROR") { }
}
public class NotFoundException : AppException
{
public NotFoundException(string message) : base(message, "NOT_FOUND") { }
}
3. Validation Error Handling
Validation errors should not trigger the global exception handler as exceptions. They are expected flows. Use IEndpointFilter for Minimal APIs or ModelState mapping for Controllers to return 400 Bad Request with structured validation errors before the request reaches business logic.
Minimal API Filter Example:
public class ValidationFilter : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
var arguments = context.Arguments;
foreach (var arg in arguments)
{
if (arg is IValidatableObject validatable)
{
var results = validatable.Validate(new ValidationContext(validatable)).ToList();
if (results.Any())
{
var errors = results.ToDictionary(
r => r.MemberNames.FirstOrDefault() ?? "General",
r => r.ErrorMessage
);
return Results.ValidationProblem(errors);
}
}
}
return await next.Invoke(context);
}
}
4. Correlation ID Propagation
Error handling is ineffective without traceability. Inject a correlation ID middleware that ensures every request has a unique identifier, propagated to logs and responses.
public class CorrelationIdMiddleware
{
private readonly RequestDelegate _next;
private const string HEADER_NAME = "X-Correlation-ID";
public CorrelationIdMiddleware(RequestDelegate next) => _next = next;
public async Task Invoke(HttpContext context)
{
var correlationId = context.Request.Headers[HEADER_NAME].FirstOrDefault()
?? Guid.NewGuid().ToString("N");
context.Items["CorrelationId"] = correlationId;
context.Response.OnStarting(() => {
context.Response.Headers[HEADER_NAME] = correlationId;
return Task.CompletedTask;
});
// Enrich logger
using (LogContext.PushProperty("CorrelationId", correlationId))
{
await _next(context);
}
}
}
5. Configuration and Registration
Register components in Program.cs with strict ordering. Error handling middleware must be among the first registered.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails(options =>
{
options.CustomizeProblemDetails = ctx =>
{
ctx.ProblemDetails.Extensions.Add("traceId", ctx.HttpContext.TraceIdentifier);
};
});
var app = builder.Build();
// Order is critical
app.UseMiddleware<CorrelationIdMiddleware>();
app.UseMiddleware<ErrorHandlerMiddleware>();
app.MapGet("/error-demo", () => {
throw new ValidationException("Invalid input parameters.");
}).AddEndpointFilter<ValidationFilter>();
app.Run();
Pitfall Guide
-
Catching Exception and Swallowing:
- Mistake: Catching
Exception in business logic, logging it, and returning null or a default value without indicating failure.
- Impact: Silent failures. The client receives success, but data is missing or corrupted. Debugging requires log forensics.
- Fix: Always propagate errors or return a failure result. Let the global handler manage the response.
-
Leaking Stack Traces in Production:
- Mistake: Returning
exception.ToString() in the response body regardless of environment.
- Impact: Information disclosure vulnerability. Attackers gain insight into internal framework versions, file paths, and logic flow.
- Fix: Use
_environment.IsDevelopment() checks or configuration flags to sanitize error details in non-development environments.
-
Returning 200 OK with Error Payload:
- Mistake: Wrapping errors in a response envelope like
{ "success": false, "message": "Error" } with HTTP 200.
- Impact: Violates HTTP semantics. Clients and proxies cannot use standard status code logic for retries or caching. Breaks API gateways and monitoring tools.
- Fix: Use appropriate HTTP status codes (4xx for client errors, 5xx for server errors).
-
Logging PII in Exception Messages:
- Mistake: Including user input directly in exception messages that are logged.
- Impact: GDPR/CCPA compliance violations. Logs may contain passwords, emails, or credit card numbers.
- Fix: Sanitize exception messages before logging. Use placeholders like
{UserId} instead of actual values.
-
Ignoring Validation Errors Globally:
- Mistake: Relying on manual
if (!ModelState.IsValid) checks in every controller action.
- Impact: Inconsistent error formats. High code duplication. Risk of missing validation checks.
- Fix: Use
IEndpointFilter, IActionFilter, or ProblemDetailsFactory to handle validation errors automatically.
-
Performance Overhead in Hot Paths:
- Mistake: Performing expensive serialization or heavy logging inside the exception handler for high-throughput endpoints.
- Impact: Increased latency during error spikes. Error handling can become the bottleneck.
- Fix: Optimize logging levels. Use asynchronous logging. Avoid complex string concatenation in error paths.
-
Middleware Ordering Errors:
- Mistake: Registering error handling middleware after routing or authentication.
- Impact: Errors occurring during routing or authentication may bypass the handler, resulting in default framework error pages.
- Fix: Place error handling middleware immediately after
UseRouting or at the very start of the pipeline for global coverage.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Minimal API Project | ErrorHandlerMiddleware + IEndpointFilter | Lightweight, aligns with minimal API philosophy, low overhead. | Low |
| MVC / Razor Pages | UseExceptionHandler + IActionFilter | Leverages MVC pipeline, integrates with ModelState. | Low |
| Public API Gateway | Global Middleware + Strict ProblemDetails | Ensures consistent RFC 7807 responses, hides internal topology. | Medium (Setup) |
| High-Throughput Microservice | Async Logging + Middleware | Prevents blocking I/O during error handling, maintains throughput. | Medium |
| Legacy .NET Framework Port | UseExceptionHandler + Custom Error View | Easier migration path, reuses existing error views. | Low |
Configuration Template
Copy this template into Program.cs for a complete setup.
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http.Features;
using System.Net.Mime;
var builder = WebApplication.CreateBuilder(args);
// 1. Register Services
builder.Services.AddLogging(logging =>
{
logging.ClearProviders();
logging.AddConsole();
// Add Serilog or other providers here
});
builder.Services.AddProblemDetails(options =>
{
options.CustomizeProblemDetails = ctx =>
{
ctx.ProblemDetails.Extensions["traceId"] = ctx.HttpContext.TraceIdentifier;
ctx.ProblemDetails.Extensions["timestamp"] = DateTime.UtcNow;
};
});
var app = builder.Build();
// 2. Middleware Pipeline
// Must be early in the pipeline
app.UseExceptionHandler(appError =>
{
appError.Run(async context =>
{
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
context.Response.ContentType = MediaTypeNames.Application.Json;
var feature = context.Features.Get<IExceptionHandlerFeature>();
if (feature != null)
{
var exception = feature.Error;
var isDev = app.Environment.IsDevelopment();
var problem = new ProblemDetails
{
Status = StatusCodes.Status500InternalServerError,
Title = "An unexpected error occurred.",
Detail = isDev ? exception.ToString() : "Please contact support with the trace ID.",
Instance = context.Request.Path,
Extensions =
{
["traceId"] = context.TraceIdentifier,
["errorCode"] = "INTERNAL_ERROR"
}
};
// Log the exception
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogError(exception, "Unhandled exception at {Path}", context.Request.Path);
await context.Response.WriteAsJsonAsync(problem);
}
});
});
app.UseRouting();
app.UseAuthorization();
// 3. Endpoints
app.MapGet("/test", () => Results.Ok("Success"));
app.MapGet("/throw", () => throw new InvalidOperationException("Test error"));
app.Run();
Quick Start Guide
- Create Middleware: Add
ErrorHandlerMiddleware.cs to your project. Implement InvokeAsync to wrap _next in a try-catch. Map exceptions to ProblemDetails.
- Register Middleware: In
Program.cs, add app.UseMiddleware<ErrorHandlerMiddleware>(); immediately after WebApplication.CreateBuilder(args).Build().
- Define Exceptions: Create
AppException and derived types. Throw these from your service layer instead of generic exceptions.
- Verify: Run the application. Trigger an error. Inspect the response to ensure it contains
type, title, status, traceId, and no stack trace (in production mode). Check logs for the correlation ID.
Implementing this structure ensures your ASP.NET Core application handles failures with the same rigor as successful operations, providing immediate value to developers, operations teams, and API consumers.