Skip to main content

ASP.NET Core Global Error Handling: Complete Guide 2026

Learn ASP.NET Core global error handling with middleware, ProblemDetails, and logging. Copy-paste C# examples and best practices — start building resilient APIs today.

If you have ever shipped an ASP.NET Core API only to watch it leak an ugly stack trace to the client, you already know why ASP.NET Core global error handling matters. A single, centralized place to catch, log, and shape exceptions is the difference between a professional API and one that exposes internals, confuses front-end teams, and buries you in inconsistent error responses. In this tutorial you will learn how to implement global error handling in ASP.NET Core the modern way — using exception handling middleware, the IExceptionHandler interface, standardized ProblemDetails responses, and structured logging.

This guide targets .NET 8 and .NET 9 (the same patterns apply going forward into 2026), and every example is runnable. Whether you are a beginner searching "how to handle exceptions in ASP.NET Core," an intermediate developer looking for best practices, or a senior engineer wanting advanced patterns, you will find something actionable here.

Why You Need Global Error Handling in ASP.NET Core

Wrapping every controller action in try/catch is a code smell. It duplicates logic, clutters your business code, and inevitably someone forgets a block and an unhandled exception escapes. Centralized error handling solves several problems at once:

  • Consistency — every error returns the same predictable JSON shape, which front-end and mobile clients can parse reliably.
  • Security — you never leak stack traces, connection strings, or internal type names to the outside world.
  • Observability — exceptions are logged in one place with correlation IDs, so you can trace failures in production.
  • Maintainability — your controllers stay focused on business logic, not plumbing.

The goal of global error handling in ASP.NET Core is simple: catch anything that bubbles up, log it with enough context to debug, and return a clean, standards-compliant response to the caller.

Option 1: The Built-In Exception Handler Middleware

The fastest way to get started is the built-in UseExceptionHandler middleware. In production you point it at an error-handling endpoint or an inline lambda. This middleware sits at the very top of the request pipeline, so it catches exceptions thrown anywhere downstream.

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            var feature = context.Features.Get<IExceptionHandlerFeature>();
            var exception = feature?.Error;

            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            context.Response.ContentType = "application/problem+json";

            await context.Response.WriteAsJsonAsync(new ProblemDetails
            {
                Status = StatusCodes.Status500InternalServerError,
                Title = "An unexpected error occurred.",
                Detail = "Please contact support if the problem persists.",
                Instance = context.Request.Path
            });
        });
    });
}

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

Notice what we did not do: we never send exception.Message or exception.StackTrace to the client. That is intentional. In development you can use app.UseDeveloperExceptionPage() (or simply skip the custom handler) to see full details, but production responses should be sanitized.

Option 2: IExceptionHandler — The Modern .NET 8+ Approach

Since .NET 8, the recommended pattern for ASP.NET Core global error handling is the IExceptionHandler interface. It is cleaner than an inline lambda, fully testable, supports dependency injection, and lets you chain multiple handlers for different exception types. This is the approach most senior developers reach for in 2026.

Step 1: Create a Global Exception Handler

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

public sealed class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger<GlobalExceptionHandler> _logger;

    public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
    {
        _logger = logger;
    }

    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        _logger.LogError(
            exception,
            "Unhandled exception for {Method} {Path}. TraceId: {TraceId}",
            httpContext.Request.Method,
            httpContext.Request.Path,
            httpContext.TraceIdentifier);

        var problemDetails = new ProblemDetails
        {
            Status = StatusCodes.Status500InternalServerError,
            Title = "Server error",
            Detail = "An unexpected error occurred while processing your request.",
            Instance = httpContext.Request.Path
        };

        problemDetails.Extensions["traceId"] = httpContext.TraceIdentifier;

        httpContext.Response.StatusCode = problemDetails.Status.Value;
        await httpContext.Response
            .WriteAsJsonAsync(problemDetails, cancellationToken);

        // return true = we handled it; false = let the next handler try
        return true;
    }
}

Step 2: Register It in Program.cs

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

var app = builder.Build();

app.UseExceptionHandler();

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

That is the whole setup. Any unhandled exception in your controllers, minimal APIs, or downstream services now flows through TryHandleAsync. Because the handler is a DI-registered service, you can inject anything — a logger, a metrics client, a feature flag service — and unit test it in isolation.

Returning Standardized Errors with ProblemDetails

Returning raw strings or ad-hoc JSON objects for errors is a common pitfall. The industry standard is RFC 9457 (formerly RFC 7807), application/problem+json, and ASP.NET Core supports it natively through the ProblemDetails class. A conformant error looks like this:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "Server error",
  "status": 500,
  "detail": "An unexpected error occurred while processing your request.",
  "instance": "/api/orders/42",
  "traceId": "0HN7B2K9V3M1L:00000003"
}

Using ProblemDetails means every client — Angular, React, a mobile app, or another microservice — gets a machine-readable, self-documenting error contract. Calling AddProblemDetails() also upgrades the framework's automatic responses (validation failures, 404s, 401s) to the same format, giving you end-to-end consistency for free.

Mapping Domain Exceptions to Status Codes

Real applications throw meaningful exceptions: a record is not found, a user lacks permission, or a request fails validation. A single 500 for all of them is a mistake. The elegant solution is to map exception types to the correct HTTP status code inside your handler. Chaining multiple IExceptionHandler implementations keeps this clean.

public sealed class NotFoundExceptionHandler : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        if (exception is not NotFoundException notFound)
            return false; // not our concern — pass to the next handler

        var problemDetails = new ProblemDetails
        {
            Status = StatusCodes.Status404NotFound,
            Title = "Resource not found",
            Detail = notFound.Message,
            Instance = httpContext.Request.Path
        };

        httpContext.Response.StatusCode = StatusCodes.Status404NotFound;
        await httpContext.Response
            .WriteAsJsonAsync(problemDetails, cancellationToken);

        return true;
    }
}

Register the specific handlers before the catch-all. Order matters — handlers run in registration order, and the first one to return true wins:

builder.Services.AddExceptionHandler<NotFoundExceptionHandler>();
builder.Services.AddExceptionHandler<ValidationExceptionHandler>();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>(); // fallback last

This pattern gives you fine-grained control while keeping controllers free of try/catch noise — a core best practice for scalable ASP.NET Core APIs.

Logging Exceptions the Right Way

Good error handling in ASP.NET Core is inseparable from good logging. A caught exception you do not log is a production incident you cannot diagnose. Follow these logging best practices:

  • Log the full exception object, not just ex.Message. Passing the exception as the first argument to LogError preserves the stack trace.
  • Use structured logging — named placeholders like {TraceId} and {Path} so log aggregators (Seq, Elastic, Application Insights) can index and query them.
  • Include a correlation/trace ID in both the log entry and the client response, so a user's error report maps directly to a log line.
  • Never log sensitive data such as passwords, tokens, or full request bodies containing PII.

For richer structured logging, Serilog remains the most popular choice in 2026. A minimal setup that enriches every log with the trace ID looks like this:

builder.Host.UseSerilog((context, config) =>
    config.ReadFrom.Configuration(context.Configuration)
          .Enrich.FromLogContext()
          .WriteTo.Console()
          .WriteTo.Seq("http://localhost:5341"));

With Enrich.FromLogContext(), any properties you push onto the log context — including the trace identifier — automatically appear on the exception log written by your global handler.

Common Pitfalls to Avoid

  • Leaking exception details in production. Never return exception.ToString() to clients. Keep detailed messages in your logs only.
  • Placing the middleware in the wrong order. UseExceptionHandler() must be registered early, before routing and endpoints, or downstream exceptions will slip past it.
  • Swallowing exceptions silently. Returning a clean response is only half the job — always log before you respond.
  • Forgetting AddProblemDetails(). Without it, framework-generated errors will not match your custom ProblemDetails format, breaking client consistency.
  • Catching Exception where a specific type belongs. Map known domain errors (not found, validation, unauthorized) to precise status codes; reserve 500 for genuinely unexpected failures.
  • Not testing the handler. Because IExceptionHandler is a plain injectable class, write unit tests asserting the status code and response body.

Development vs. Production Configuration

A practical setup exposes rich detail locally while sanitizing production output. Combine the developer exception page in development with your global handler everywhere else:

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

This gives your team full stack traces while debugging and safe, standardized ProblemDetails responses for real users — the best of both worlds.

Conclusion: Key Takeaways

ASP.NET Core global error handling is one of the highest-leverage improvements you can make to any API. By centralizing exception handling you gain consistency, security, and observability while keeping your controllers clean. Here is what to remember:

  • Use IExceptionHandler with AddExceptionHandler and UseExceptionHandler() — the modern, testable pattern for .NET 8 and later.
  • Return standardized ProblemDetails (RFC 9457) so every client gets a predictable error contract; always call AddProblemDetails().
  • Map domain exceptions to precise HTTP status codes by chaining specific handlers before a catch-all fallback.
  • Log the full exception with structured logging and a trace ID, and never leak internal details to clients.
  • Show rich errors in development, sanitized responses in production.

Implement these patterns today and your ASP.NET Core APIs will be more secure, more debuggable, and far more professional. Start with the IExceptionHandler example above, wire in ProblemDetails and logging, and you will have production-grade global error handling in under an hour.

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

.NET MAUI Tutorial 2026: Build Cross-Platform Apps in C#

Learn .NET MAUI in 2026 to build iOS, Android, Windows & Mac apps from one C# codebase. Start this cross-platform tutorial with code examples today. .NET MAUI (Multi-platform App UI) is Microsoft's framework for building native iOS, Android, Windows, and macOS apps from a single C# codebase . If you've ever wanted to ship a mobile app without learning Swift, Kotlin, and Win32 separately, this .NET MAUI tutorial for 2026 is your starting point. In this guide you'll learn what .NET MAUI is, why it matters for cross-platform app development in C#, and how to build your first working app — with runnable code examples and the best practices senior engineers actually use in production. What Is .NET MAUI and Why Use It in 2026? .NET MAUI is the evolution of Xamarin.Forms, fully integrated into the modern .NET runtime. With one project and one language — C# — you target four platforms. The framework compiles to native UI controls on each device, so a button on iOS...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...