Error Handling, Logging, and Observability

This section documents ASP.NET Core on .NET 10 (LTS), the current release — the minimal hosting model, the middleware pipeline, dependency injection, Minimal APIs, MVC & Razor Pages, Blazor with the current render modes, SignalR and gRPC, EF Core, ASP.NET Core Identity and policy-based authorization, output caching, rate limiting, and Native-AOT-aware building — as described by the official documentation at Microsoft Learn, which is the reference these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. .NET ships a major release every November and its APIs continue to evolve: the examples here target .NET 10 / C# 14.

This section’s bibliography lists the reference material consulted while preparing these pages.

An app in production needs to fail cleanly, record what happened, and expose its health. This page covers all three.

Error handling

if (app.Environment.IsDevelopment())
    app.UseDeveloperExceptionPage();     // full stack trace -- Development only
else
{
    app.UseExceptionHandler("/error");   // or a lambda handler
    app.UseStatusCodePagesWithReExecute("/error/{0}");
}

builder.Services.AddProblemDetails();    // RFC 9457 bodies for both exceptions and status codes

Prefer a typed IExceptionHandler (they run in order until one handles the exception):

public sealed class NotFoundExceptionHandler : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(HttpContext ctx, Exception ex, CancellationToken ct)
    {
        if (ex is not EntityNotFoundException nf) return false;
        ctx.Response.StatusCode = StatusCodes.Status404NotFound;
        await ctx.Response.WriteAsJsonAsync(new ProblemDetails { Title = nf.Message, Status = 404 }, ct);
        return true;
    }
}

builder.Services.AddExceptionHandler<NotFoundExceptionHandler>();

The DatabaseDeveloperPageExceptionFilter (with AddDatabaseDeveloperPageExceptionFilter) turns pending-EF-migration errors into an actionable page in Development. See Handle errors in ASP.NET Core and web API error handling.

The Operation Result pattern

For expected failures (validation, "not found", business-rule rejections), returning a result object is cheaper and clearer than throwing:

public readonly record struct Result<T>(bool Ok, T? Value, string? Error)
{
    public static Result<T> Success(T value) => new(true, value, null);
    public static Result<T> Fail(string error) => new(false, default, error);
}

// endpoint translates the result to HTTP
app.MapGet("/orders/{id:int}", (int id, IOrderService svc) =>
{
    var r = svc.Get(id);
    return r.Ok ? Results.Ok(r.Value) : Results.Problem(r.Error, statusCode: 404);
});

Reserve exceptions for unexpected conditions. See Architecture and patterns for how this composes with ProblemDetails.

Logging

ILogger<T> is injected everywhere. Use message templates with named placeholders — the values are captured as structured fields, not just interpolated text.

public sealed class OrderService(ILogger<OrderService> logger)
{
    public async Task PlaceAsync(Order order)
    {
        using var scope = logger.BeginScope("OrderId:{OrderId}", order.Id);
        logger.LogInformation("Placing order for {Customer} totalling {Total:C}", order.Customer, order.Total);
        // ...
        logger.LogWarning("Inventory low for {Sku}", sku);
    }
}

The LoggerMessage source generator makes hot-path logging allocation-free:

internal static partial class Log
{
    [LoggerMessage(Level = LogLevel.Information, Message = "Placed order {OrderId} in {ElapsedMs} ms")]
    public static partial void OrderPlaced(ILogger logger, int orderId, long elapsedMs);
}

Providers: Console, Debug, EventSource, ApplicationInsights, and third-party sinks such as Serilog (builder.Services.AddSerilog(…​)). Levels and category filters come from the Logging config section. See Logging in .NET Core and ASP.NET Core.

HTTP logging

builder.Services.AddHttpLogging(o =>
    o.LoggingFields = HttpLoggingFields.RequestPath | HttpLoggingFields.ResponseStatusCode | HttpLoggingFields.Duration);

app.UseHttpLogging();          // structured request/response log entries
app.UseW3CLogging();           // W3C Extended Log File Format to disk

Use IHttpLoggingInterceptor to add or redact fields per request. See HTTP logging in ASP.NET Core.

Health checks

builder.Services.AddHealthChecks()
    .AddDbContextCheck<AppDbContext>(tags: ["ready"])
    .AddUrlGroup(new Uri("https://payments.example.com/health"), name: "payments", tags: ["ready"]);

app.MapHealthChecks("/healthz/live");                                   // process is up
app.MapHealthChecks("/healthz/ready", new() { Predicate = c => c.Tags.Contains("ready") });  // dependencies OK

Implement IHealthCheck for a custom probe. Liveness answers "restart me?"; readiness answers "send me traffic?". See Health checks in ASP.NET Core.

Metrics and tracing

ASP.NET Core emits metrics and Activity traces via System.Diagnostics. Export them with OpenTelemetry:

builder.Services.AddOpenTelemetry()
    .WithMetrics(m => m.AddAspNetCoreInstrumentation().AddRuntimeInstrumentation().AddOtlpExporter())
    .WithTracing(t => t.AddAspNetCoreInstrumentation().AddHttpClientInstrumentation()
        .AddEntityFrameworkCoreInstrumentation().AddOtlpExporter());

Create your own instruments with Meter / Counter<T> / Histogram<T>, and custom spans with ActivitySource. Distributed tracing propagates the traceparent header across services automatically. See Metrics for ASP.NET Core apps.