In traditional web applications, unhandled exceptions often result in crude HTML stack traces, unstructured JSON error objects, or generic HTTP 500 Internal Server Error responses. This lack of consistency forces frontend developers and downstream API consumers to guess what went wrong, complicating debugging and client-side error handling.
RFC 7807 Problem Details solves this by defining a standardized, machine-readable JSON format for communicating errors across HTTP APIs. Combined with modern built-in exception handling features in ASP.NET Core, you can centralize error interception, sanitize sensitive server data, and return predictable, standardized error payloads.
This article walks through a complete, production-grade implementation of global exception handling using IExceptionHandler and RFC 7807 in an ASP.NET Core Web API.
1. What is RFC 7807 Problem Details?
The RFC 7807 specification standardizes error responses so clients don't have to parse different error schemas for every endpoint. A standard Problem Details response includes the following properties:
type: A URI reference that identifies the problem type (e.g., a documentation link).title: A short, human-readable summary of the problem type.status: The HTTP status code generated by the origin server for this occurrence of the problem.detail: A human-readable explanation specific to this occurrence of the problem.instance: A URI reference that identifies the specific occurrence of the problem (often the request path).
2. Step 1: Defining Custom Domain Exceptions
Before handling errors globally, establish clear, semantic custom exceptions within your application layer. This allows you to differentiate between a missing resource, a validation failure, and an unexpected system crash.
C#
// Application/Common/Exceptions/NotFoundException.cs
namespace Application.Common.Exceptions;
public class NotFoundException : Exception
{
public NotFoundException(string name, object key)
: base($"Entity \"{name}\" ({key}) was not found.") { }
}
// Application/Common/Exceptions/ValidationException.cs
using FluentValidation.Results;
namespace Application.Common.Exceptions;
public class ValidationException : Exception
{
public IDictionary<string, string[]> Errors { get; }
public ValidationException(IEnumerable<ValidationFailure> failures)
: base("One or more validation failures have occurred.")
{
Errors = failures
.GroupBy(e => e.PropertyName, e => e.ErrorMessage)
.ToDictionary(failureGroup => failureGroup.Key, failureGroup => failureGroup.ToArray());
}
}
3. Step 2: Implementing the Modern IExceptionHandler
Starting with .NET 8, ASP.NET Core introduced the IExceptionHandler interface. This provides a clean, pipeline-native way to intercept exceptions globally without writing custom inline middleware lambda blocks.
C#
// WebApi/Middleware/GlobalExceptionHandler.cs
using Application.Common.Exceptions;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
namespace WebApi.Middleware;
public class GlobalExceptionHandler : IExceptionHandler
{
readonly ILogger<GlobalExceptionHandler> _logger;
public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
{
_logger = logger;
}
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
_logger.LogError(exception, "An unhandled exception occurred: {Message}", exception.Message);
var problemDetails = new ProblemDetails
{
Instance = httpContext.Request.Path
};
// Map specific exceptions to HTTP status codes and problem details
switch (exception)
{
case NotFoundException notFound:
problemDetails.Status = StatusCodes.Status404NotFound;
problemDetails.Title = "Resource Not Found";
problemDetails.Detail = notFound.Message;
break;
case ValidationException validation:
problemDetails.Status = StatusCodes.Status400BadRequest;
problemDetails.Title = "Validation Failed";
problemDetails.Detail = "One or more validation errors occurred.";
problemDetails.Extensions["errors"] = validation.Errors;
break;
case UnauthorizedAccessException:
problemDetails.Status = StatusCodes.Status401Unauthorized;
problemDetails.Title = "Unauthorized";
problemDetails.Detail = "Authentication is required to access this resource.";
break;
default:
problemDetails.Status = StatusCodes.Status500InternalServerError;
problemDetails.Title = "Internal Server Error";
problemDetails.Detail = "An unexpected error occurred while processing your request.";
break;
}
httpContext.Response.StatusCode = problemDetails.Status.Value;
httpContext.Response.ContentType = "application/problem+json";
await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
// Return true to signal that the exception has been handled
return true;
}
}
4. Step 3: Registering the Exception Handler in Program.cs
Register your handler with the dependency injection container and configure the exception handling middleware pipeline.
C#
// WebApi/Program.cs
using WebApi.Middleware;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Register the custom global exception handler
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails(); // Required for standardized problem details generation
var app = builder.Build();
// Configure the HTTP request pipeline
// Must be placed early in the pipeline to catch downstream errors
app.UseExceptionHandler();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
5. Step 4: Example Response Output
When a NotFoundException is thrown inside your application layer, the client receives a clean, predictable RFC 7807 JSON response:
JSON
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
"title": "Resource Not Found",
"status": 404,
"detail": "Entity \"Product\" (42) was not found.",
"instance": "/api/products/42"
}
Conclusion
By implementing global exception handling with IExceptionHandler and RFC 7807 Problem Details, your ASP.NET Core Web API eliminates unstructured error responses. This shields internal implementation details from external clients while providing a predictable, professional contract for frontend developers.

Join the conversation! Your thoughts help the community grow.