ASP.NET Core  

Implementing Enterprise-Grade Global Exception Handling via Custom Middleware

Proper error handling is vital for robust production applications. Instead of wrapping every controller action or minimal endpoint handler in repetitive try-catch blocks, .NET Core allows you to capture unhandled exceptions globally through custom middleware or centralized exception handlers.

This approach ensures a consistent JSON error response schema across your entire API ecosystem.

Step 1: Create a Standardized Error Response Model

Define a predictable structure for your error payload so that frontend clients can parse failures uniformly.

C#

public class ErrorDetails
{
    public int StatusCode { get; set; }
    public string Message { get; set; } = string.Empty;
    public string? Details { get; set; }
    public DateTime Timestamp { get; set; } = DateTime.UtcNow;

    public override string ToString() => System.Text.Json.JsonSerializer.Serialize(this);
}

Step 2: Build the Custom Exception Handling Middleware

Create a middleware component that intercepts exceptions flowing down the HTTP pipeline, logs them securely, and writes a uniform JSON payload back to the client.

C#

using System.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;

public class GlobalExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<GlobalExceptionHandlingMiddleware> _logger;
    private readonly IWebHostEnvironment _env;

    public GlobalExceptionHandlingMiddleware(
        RequestDelegate next, 
        ILogger<GlobalExceptionHandlingMiddleware> logger, 
        IWebHostEnvironment env)
    {
        _next = next;
        _logger = logger;
        _env = env;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            // Move to the next middleware in the pipeline
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An unhandled exception occurred during execution: {Message}", ex.Message);
            await HandleExceptionAsync(context, ex);
        }
    }

    private async Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        context.Response.ContentType = "application/json";
        
        // Default to Internal Server Error
        var statusCode = (int)HttpStatusCode.InternalServerError;
        var message = "An internal server error has occurred.";

        // Custom domain or validation exceptions can be handled explicitly here
        if (exception is KeyNotFoundException)
        {
            statusCode = (int)HttpStatusCode.NotFound;
            message = "The requested resource was not found.";
        }
        else if (exception is UnauthorizedAccessException)
        {
            statusCode = (int)HttpStatusCode.Unauthorized;
            message = "Authentication is required to access this resource.";
        }

        context.Response.StatusCode = statusCode;

        var errorDetails = new ErrorDetails
        {
            StatusCode = statusCode,
            Message = message,
            // Expose stack trace details only if running in Development environment
            Details = _env.IsDevelopment() ? exception.StackTrace : null
        };

        await context.Response.WriteAsync(errorDetails.ToString());
    }
}

Step 3: Register an Extension Method for Clean Setup

To keep Program.cs clean and modular, wrap the middleware registration in an extension class.

C#

public static class MiddlewareExtensions
{
    public static IApplicationBuilder UseGlobalExceptionHandling(this IApplicationBuilder app)
    {
        return app.UseMiddleware<GlobalExceptionHandlingMiddleware>();
    }
}

Step 4: Wire Up the Middleware in Program.cs

Register your custom middleware at the very beginning of the HTTP request pipeline so it wraps all subsequent operations (such as routing, endpoint execution, and authentication).

C#

var builder = WebApplication.CreateBuilder(args);

// Add services...
builder.Services.AddControllers();

var app = builder.Build();

// 1. Place the exception handling middleware first in the pipeline execution tree
app.UseGlobalExceptionHandling();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

// Test endpoint designed to throw an exception on purpose
app.MapGet("/error-test", () =>
{
    throw new InvalidOperationException("Simulated catastrophic failure for middleware testing.");
});

app.Run();

Step 5: Test the Global Handler

Run your application (dotnet run) and navigate to https://localhost:{port}/error-test.

Instead of an unhandled server crash, raw HTML error dump, or dropped connection, the pipeline intercepts the InvalidOperationException and safely responds with a structured, production-ready JSON error object:

JSON

{
  "statusCode": 500,
  "message": "An internal server error has occurred.",
  "details": "   at Program.<>c.<__ अगदी line>...",
  "timestamp": "2026-08-03T09:00:00Z"
}