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
| Piece | What it does |
|---|---|
| UseExceptionHandler | Catches unhandled exceptions, logs them, and produces a response through a path, a lambda or ProblemDetails |
| AddProblemDetails | Registers the default IProblemDetailsService so exception handling, status code pages and the developer page can emit ProblemDetails responses |
| IExceptionHandler | An 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
- Stack traces, file paths and framework versions.
- SQL text, connection strings or table names.
- Raw exception messages from libraries, which may echo user input or internal addresses.
- Whether a user exists, in login and reset flows. Use the same message for every failure.
Steps
- Confirm the Developer Exception Page is only wired for Development.
- Add AddProblemDetails and a production exception handler.
- Map known exceptions to specific 4xx codes in one IExceptionHandler.
- Return a trace identifier and log everything else server side.
- Test with the environment set to Production and a forced exception.
- 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.