Problem Details and RFC 9457 in ASP.NET Core

Every API needs a consistent error format. Without one, clients get a mix of plain text messages, HTML error pages, and ad-hoc JSON objects depending on what went wrong and where. RFC 9457 (formerly RFC 7807) defines Problem Details — a standard JSON structure for conveying machine-readable error information from HTTP APIs.

The Problem Details Format

A Problem Details response looks like this:

data.json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
  "title": "Not Found",
  "status": 404,
  "detail": "Product with ID 42 was not found.",
  "instance": "/api/products/42"
}

The key fields are:

Enabling Problem Details in ASP.NET Core

ASP.NET Core 7+ has built-in support. Enable it with a single line:

Program.cs
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

Now unhandled exceptions return Problem Details JSON instead of HTML error pages, and empty status code responses (like a bare 404) get a Problem Details body.

Returning Problem Details from Controllers

The ControllerBase class includes helper methods:

ProductsController.cs
[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
    private readonly IProductRepository _repository;

    public ProductsController(IProductRepository repository)
    {
        _repository = repository;
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetById(int id)
    {
        var product = await _repository.GetByIdAsync(id);

        if (product is null)
        {
            return Problem(
                detail: $"Product with ID {id} was not found.",
                statusCode: StatusCodes.Status404NotFound,
                title: "Product Not Found");
        }

        return Ok(product);
    }

    [HttpPost]
    public async Task<IActionResult> Create(CreateProductRequest request)
    {
        if (await _repository.ExistsBySkuAsync(request.Sku))
        {
            return Problem(
                detail: $"A product with SKU '{request.Sku}' already exists.",
                statusCode: StatusCodes.Status409Conflict,
                title: "Duplicate SKU");
        }

        var product = await _repository.CreateAsync(request);
        return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
    }
}

Returning Problem Details from Minimal APIs

Use TypedResults.Problem or Results.Problem:

Example.cs
app.MapGet("/api/products/{id}", async (int id, IProductRepository repository) =>
{
    var product = await repository.GetByIdAsync(id);

    return product is not null
        ? Results.Ok(product)
        : Results.Problem(
            detail: $"Product with ID {id} was not found.",
            statusCode: StatusCodes.Status404NotFound,
            title: "Product Not Found");
});

Validation Errors

The [ApiController] attribute automatically returns validation errors as Problem Details with the application/problem+json content type. The response uses the ValidationProblemDetails subclass, which includes an errors dictionary:

data.json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "Name": ["The Name field is required."],
    "Price": ["The Price field must be greater than 0."]
  }
}

Custom Problem Types

Define your own problem types for domain-specific errors:

ProblemTypes.cs
public static class ProblemTypes
{
    public const string InsufficientStock = "https://api.example.com/problems/insufficient-stock";
    public const string PaymentDeclined = "https://api.example.com/problems/payment-declined";
}
Example.cs
[HttpPost("orders")]
public IActionResult PlaceOrder(PlaceOrderRequest request)
{
    if (stock < request.Quantity)
    {
        return Problem(
            type: ProblemTypes.InsufficientStock,
            detail: $"Requested {request.Quantity} units but only {stock} available.",
            statusCode: StatusCodes.Status422UnprocessableEntity,
            title: "Insufficient Stock");
    }

    // ...
}

Customising the Problem Details Service

Use CustomizeProblemDetails to add information to every Problem Details response:

Program.cs
builder.Services.AddProblemDetails(options =>
{
    options.CustomizeProblemDetails = context =>
    {
        context.ProblemDetails.Instance =
            $"{context.HttpContext.Request.Method} {context.HttpContext.Request.Path}";

        context.ProblemDetails.Extensions["traceId"] =
            Activity.Current?.Id ?? context.HttpContext.TraceIdentifier;

        context.ProblemDetails.Extensions["timestamp"] = DateTime.UtcNow;
    };
});

Every error response now includes the trace ID and timestamp, making support requests much easier to trace.

Exception Handling with Problem Details

Map specific exceptions to Problem Details responses:

Example.cs
app.UseExceptionHandler(exceptionApp =>
{
    exceptionApp.Run(async context =>
    {
        var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;
        var problemDetails = exception switch
        {
            NotFoundException ex => new ProblemDetails
            {
                Status = StatusCodes.Status404NotFound,
                Title = "Resource Not Found",
                Detail = ex.Message
            },
            ConflictException ex => new ProblemDetails
            {
                Status = StatusCodes.Status409Conflict,
                Title = "Conflict",
                Detail = ex.Message
            },
            _ => new ProblemDetails
            {
                Status = StatusCodes.Status500InternalServerError,
                Title = "Internal Server Error",
                Detail = "An unexpected error occurred."
            }
        };

        context.Response.StatusCode = problemDetails.Status ?? 500;
        await context.Response.WriteAsJsonAsync(problemDetails);
    });
});

Problem Details isn't glamorous, but it transforms your API's error handling from ad-hoc to consistent. Clients can parse errors reliably, support teams can trace issues efficiently, and your API behaves predictably regardless of what goes wrong.