What's the Best Way to Handle Global Exception Handling in .NET Core?

Asked 1 year ago Updated 18 days ago 1377 views

1 Answer


1

Global exception handling in .NET Core / ASP.NET Core is a centralized mechanism for catching unhandled exceptions, logging them, and returning consistent error responses to clients. Instead of wrapping every controller action in try-catch blocks, you configure a single global exception handler that processes all unexpected errors.

Why Use Global Exception Handling?

Implementing global exception handling provides several advantages:

  • Reduces repetitive try-catch code.
  • Keeps controllers and services clean.
  • Returns consistent API responses.
  • Logs errors in one place.
  • Prevents sensitive exception details from being exposed.
  • Improves application maintainability.

Option 1: Use Built-in Exception Handler Middleware (Recommended)

ASP.NET Core provides built-in middleware for handling unhandled exceptions.

Program.cs (.NET 6+)

// Add middleware before routing
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/error");
}
else
{
    app.UseDeveloperExceptionPage();
}

Create an Error Controller:

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

[ApiController]
public class ErrorController : ControllerBase
{
    [Route("/error")]
    public IActionResult HandleError()
    {
        // Get exception details
        var context = HttpContext.Features.Get<IExceptionHandlerFeature>();

        // Log exception here if needed

        return Problem(
            title: "An unexpected error occurred.",
            statusCode: 500
        );
    }
}

Advantages

  • Built into ASP.NET Core
  • Easy to configure
  • Works well for MVC and Web APIs
  • Supports Problem Details (RFC 7807)

Option 2: Custom Exception Middleware (Most Flexible)

For enterprise applications, creating custom middleware offers greater control.

Custom Middleware

using System.Net;
using System.Text.Json;

public class ExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ExceptionMiddleware> _logger;

    // Constructor
    public ExceptionMiddleware(RequestDelegate next,
                               ILogger<ExceptionMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    // Middleware execution
    public async Task Invoke(HttpContext context)
    {
        try
        {
            // Continue request pipeline
            await _next(context);
        }
        catch (Exception ex)
        {
            // Log exception
            _logger.LogError(ex, ex.Message);

            // Set response
            context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
            context.Response.ContentType = "application/json";

            var response = new
            {
                Success = false,
                Message = "Something went wrong.",
                StatusCode = 500
            };

            await context.Response.WriteAsync(
                JsonSerializer.Serialize(response));
        }
    }
}

Register Middleware

app.UseMiddleware<ExceptionMiddleware>();

Response

{
    "success": false,
    "message": "Something went wrong.",
    "statusCode": 500
}

Option 3: Exception Filters

Exception filters work only within the MVC pipeline.

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

public class GlobalExceptionFilter : IExceptionFilter
{
    public void OnException(ExceptionContext context)
    {
        context.Result = new ObjectResult(new
        {
            Message = "Server Error"
        })
        {
            StatusCode = 500
        };

        context.ExceptionHandled = true;
    }
}

Register:

builder.Services.AddControllers(options =>
{
    options.Filters.Add<GlobalExceptionFilter>();
});

Returning Different Status Codes

It's a good practice to map exception types to appropriate HTTP status codes.

switch (exception)
{
    case ArgumentException:
        statusCode = StatusCodes.Status400BadRequest;
        break;

    case UnauthorizedAccessException:
        statusCode = StatusCodes.Status401Unauthorized;
        break;

    case KeyNotFoundException:
        statusCode = StatusCodes.Status404NotFound;
        break;

    default:
        statusCode = StatusCodes.Status500InternalServerError;
        break;
}

Example responses:

400 Bad Request

{
    "message": "Invalid input."
}

404 Not Found

{
    "message": "Resource not found."
}

500 Internal Server Error

{
    "message": "Unexpected server error."
}

Logging Exceptions

Use the built-in logging framework rather than writing logs manually.

_logger.LogError(exception,
    "Error occurred while processing request.");

You can integrate logging providers such as:

  • Console Logger
  • Debug Logger
  • Event Log
  • Azure Application Insights
  • Serilog
  • NLog

Use ProblemDetails for APIs

ASP.NET Core supports RFC 7807 Problem Details, which provides a standard error format.

return Results.Problem(
    title: "Unexpected Error",
    detail: exception.Message,
    statusCode: 500
);

Example:

{
    "type": "about:blank",
    "title": "Unexpected Error",
    "status": 500,
    "detail": "Database connection failed."
}

In production, avoid returning internal exception messages to clients; log the detailed exception and return a generic detail instead.

Best Practices

  • Use centralized exception handling instead of repetitive try-catch blocks.
  • Prefer middleware for application-wide exception handling.
  • Return appropriate HTTP status codes for different exception types.
  • Log full exception details, including stack traces, to a logging provider.
  • Avoid exposing sensitive exception information in production responses.
  • Use the standard ProblemDetails format for REST APIs.
  • Handle expected business or validation errors separately from unexpected exceptions.
  • Place exception handling middleware early in the request pipeline so it can catch downstream exceptions.

Which Approach Should You Choose?

Approach Best For Pros Cons
Built-in Exception Handler Most applications Simple, reliable, integrates with ASP.NET Core Less customizable
Custom Middleware Enterprise APIs Full control over logging and responses More code to maintain
Exception Filters MVC-specific scenarios Easy for controller logic Doesn't catch exceptions outside the MVC pipeline

Conclusion

For most modern ASP.NET Core applications, global exception handling through middleware is the preferred solution. It centralizes error handling, produces consistent API responses, integrates cleanly with logging, and keeps business logic free from repetitive error-handling code. The built-in exception handler is sufficient for many applications, while a custom middleware is an excellent choice when you need custom error formats, exception mapping, or advanced logging behavior.

Write Your Answer