Use ProblemDetails as the public error contract for an API instead of returning ad hoc error objects from different endpoints. Clients should not have to handle one shape for validation errors, another shape for exceptions, and a third shape for business rule failures.

The value is bigger than the JSON format. Every error can carry the same core fields: status, title, detail, type, and instance. That gives clients a predictable contract while still leaving room for extensions such as trace IDs, error codes, validation keys, or support links.

In ASP.NET Core, configure problem details once and let middleware or MVC produce consistent responses for common failure paths. Then use explicit problem responses for domain-level errors where the API needs to explain what the caller can do next.

builder.Services.AddProblemDetails();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
}

app.MapPost("/api/transfer", (TransferRequest request) =>
{
    if (request.Amount > request.Balance)
    {
        return Results.Problem(
            statusCode: 400,
            title: "Insufficient balance",
            detail: "The transfer amount exceeds the available balance.",
            extensions: new Dictionary<string, object?>
            {
                ["errorCode"] = "INSUFFICIENT_FUNDS"
            });
    }

    return Results.Ok();
});

Keep the details safe for production. A problem response should describe the API error, not leak stack traces, connection strings, model prompts, dependency payloads, or internal exception messages. Log the internal detail, return the client-facing contract.