Loading...

ProblemDetails in ASP.NET Core: Clean Error Responses with TraceId

Map exceptions to RFC 7807 ProblemDetails and include a traceId so clients get consistent errors and you can match the exact request in logs.

When an exception happens in an API, the client still needs an error response that is consistent, safe, and easy to consume. Returning random JSON shapes (or stack traces) makes client code fragile and debugging painful.

A clean approach is to return errors using Problem Details (RFC 7807) and include a traceId so the failing request can be matched to the exact log entry. In .NET 8, the modern setup uses the built-in IExceptionHandler pipeline together with AddProblemDetails().


Goal


What is ProblemDetails?

ProblemDetails is a standard error payload shape with these common fields:

RFC 7807 also allows adding extra fields (extension members). This example adds traceId.


Program.cs Setup (AddProblemDetails + IExceptionHandler)

AddProblemDetails() registers the Problem Details infrastructure. AddExceptionHandler<T>() registers a handler that maps exceptions to the right ProblemDetails response.


using System.Diagnostics;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

// 1) ProblemDetails + global customization (adds traceId to all ProblemDetails responses)
builder.Services.AddProblemDetails(options =>
{
    options.CustomizeProblemDetails = ctx =>
    {
        var http = ctx.HttpContext;
        var traceId = Activity.Current?.Id ?? http.TraceIdentifier;

        // Add traceId to every ProblemDetails (including validation errors, when applicable)
        ctx.ProblemDetails.Extensions["traceId"] = traceId;

        // A useful default for instance
        ctx.ProblemDetails.Instance ??= http.Request.Path;
    };
});

// 2) Register exception handler (IExceptionHandler)
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();

var app = builder.Build();

// 3) Add the built-in exception handling middleware
app.UseExceptionHandler();

app.MapControllers();
app.Run();

GlobalExceptionHandler (Map Exceptions to Status Codes)

IExceptionHandler lets the application centralize exception-to-response mapping. Known exceptions get meaningful HTTP statuses (404/409/400). Unknown exceptions return 500 with a safe message.


using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;

public sealed class GlobalExceptionHandler(
    ILogger<GlobalExceptionHandler> logger,
    IProblemDetailsService problemDetailsService) : IExceptionHandler
{
    private readonly ILogger<GlobalExceptionHandler> _logger = logger;
    private readonly IProblemDetailsService _problemDetailsService = problemDetailsService;

    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        var (status, title, type, detail) = Map(exception);

        // Log full details server-side
        _logger.LogError(exception, "Unhandled exception ({Status}).", status);

        var problem = new ProblemDetails
        {
            Status = status,
            Title = title,
            Type = type,
            Detail = detail,
            Instance = httpContext.Request.Path
        };

        httpContext.Response.StatusCode = status;

        // This writes "application/problem+json" and applies AddProblemDetails customization (traceId, etc.)
        return await _problemDetailsService.TryWriteAsync(new ProblemDetailsContext
        {
            HttpContext = httpContext,
            ProblemDetails = problem,
            Exception = exception
        });
    }

    private static (int status, string title, string type, string detail) Map(Exception ex)
    {
        return ex switch
        {
            NotFoundException nf => (StatusCodes.Status404NotFound, "Not found", "https://httpstatuses.com/404", nf.Message),
            ConflictException cf => (StatusCodes.Status409Conflict, "Conflict", "https://httpstatuses.com/409", cf.Message),
            ValidationException ve => (StatusCodes.Status400BadRequest, "Validation error", "https://httpstatuses.com/400", ve.Message),

            // Unknown/unexpected
            _ => (StatusCodes.Status500InternalServerError, "Unexpected error", "https://httpstatuses.com/500",
                  "An unexpected error occurred.")
        };
    }
}

Example Exceptions

Small, explicit exception types make mapping simple and readable:


public sealed class NotFoundException(string message) : Exception(message);
public sealed class ConflictException(string message) : Exception(message);
public sealed class ValidationException(string message) : Exception(message);

Example Output

The response keeps a predictable shape and includes traceId:


{
  "type": "https://httpstatuses.com/404",
  "title": "Not found",
  "status": 404,
  "detail": "Customer was not found.",
  "instance": "/api/customers/42",
  "traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}
ASP.NET Core Clean Error Responses with TraceId

Client Side: Use traceId for Support

Client code can show a friendly message and keep traceId as a reference for troubleshooting:


const res = await fetch("/api/customers/42");

if (!res.ok) {
  const contentType = res.headers.get("Content-Type") || "";
  let payload;

  if (contentType.includes("application/problem+json") || contentType.includes("application/json")) {
    payload = await res.json();
  } else {
    // Fallback: HTML/plain-text gibi durumlar
    payload = { title: "Unexpected error", detail: await res.text() };
  }

  console.error("API error:", payload);

  // ProblemDetails ise traceId bulunabilir (yoksa boş geçilir)
  const traceId = payload?.traceId || payload?.extensions?.traceId;

  alert(`Something went wrong.${traceId ? ` Reference: ${traceId}` : ""}`);
}

Common Improvements


TL;DR

  • Use ProblemDetails (RFC 7807) for consistent error responses.
  • In .NET 8, prefer IExceptionHandler + AddProblemDetails() for a modern setup.
  • Add traceId (via CustomizeProblemDetails) to correlate client errors with logs.
  • Do not expose stack traces in production.