Safeguard
Security Guides•

How should an ASP.NET Core API handle errors in production?

Use UseExceptionHandler with AddProblemDetails in production, keep the Developer Exception Page for Development only, and log details server side.

Safeguard Security Team
4 min read

In production, call UseExceptionHandler, register AddProblemDetails so errors return a standard ProblemDetails body, and keep the Developer Exception Page for the Development environment only. Log full exception details on the server and send clients a generic message with a trace identifier. Microsoft warns not to serve sensitive error information to clients.

What the Microsoft documentation says

The error handling page states that the Developer Exception Page is enabled by default only when the app runs in Development and was created with WebApplication.CreateBuilder. It also says: "Don't enable the Developer Exception Page unless the app is running in the Development environment. Don't share detailed exception information publicly when the app runs in production."

For production, UseExceptionHandler is the middleware that catches and logs unhandled exceptions and re-executes the request in an alternate pipeline. If that alternate pipeline throws, the middleware rethrows the original exception. The page repeats a warning in several sections: do not serve sensitive error information to clients, because serving errors is a security risk. It adds that production error pages must be tested carefully so they do not throw exceptions of their own.

Three building blocks

PieceWhat it does
UseExceptionHandlerCatches unhandled exceptions, logs them, and produces a response through a path, a lambda or ProblemDetails
AddProblemDetailsRegisters the default IProblemDetailsService so exception handling, status code pages and the developer page can emit ProblemDetails responses
IExceptionHandlerAn interface for handling known exception types in one place, registered with AddExceptionHandler

For IExceptionHandler, the docs say handlers run only if UseExceptionHandler is called, they are tried in order until one returns true from TryHandleAsync, and they have singleton lifetime. A handler that returns true without writing a response produces a 404, so write the whole response. Starting in .NET 10, the page says diagnostics such as logs and metrics are suppressed by default for handled exceptions, with a callback to change that. If you rely on logs for alerting, confirm what is recorded after an upgrade.

A minimal production setup

builder.Services.AddProblemDetails();
builder.Services.AddExceptionHandler<ApiExceptionHandler>();

var app = builder.Build();

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

Calling UseExceptionHandler with no arguments needs a fallback, otherwise startup throws and tells you to set a path or handler. AddProblemDetails satisfies that.

public sealed class ApiExceptionHandler(ILogger<ApiExceptionHandler> log) : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(
        HttpContext ctx, Exception ex, CancellationToken ct)
    {
        log.LogError(ex, "Unhandled error {TraceId}", ctx.TraceIdentifier);
        ctx.Response.StatusCode = StatusCodes.Status500InternalServerError;
        await ctx.Response.WriteAsJsonAsync(new ProblemDetails
        {
            Status = 500,
            Title = "An error occurred while processing your request.",
            Extensions = { ["traceId"] = ctx.TraceIdentifier }
        }, ct);
        return true;
    }
}

The client sees a title and a trace identifier. The stack trace stays in your log. OWASP's error handling guidance agrees: return a generic response and record details server side.

What not to put in an error response

  1. Stack traces, file paths and framework versions.
  2. SQL text, connection strings or table names.
  3. Raw exception messages from libraries, which may echo user input or internal addresses.
  4. Whether a user exists, in login and reset flows. Use the same message for every failure.

Steps

  1. Confirm the Developer Exception Page is only wired for Development.
  2. Add AddProblemDetails and a production exception handler.
  3. Map known exceptions to specific 4xx codes in one IExceptionHandler.
  4. Return a trace identifier and log everything else server side.
  5. Test with the environment set to Production and a forced exception.
  6. Run a dynamic scan against a staging build to look for leaked detail.

Try it yourself

Click through it at your own pace, or open it full screen. Watch for findings about verbose error output.

Quick check

Is the ASPNETCORE_ENVIRONMENT value really that important?

Yes. The developer page appears by default only in Development, so a production host mistakenly set to Development exposes details. Check the variable in every deployment.

Can I put the exception message in the ProblemDetails detail field?

Only for exceptions you wrote and designed for clients. Never for unexpected ones.

Safeguard dependency scanning and SBOM generation cover the packages in your ASP.NET Core service, and the free scan is at gold.safeguard.sh/scan.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.