← Back to Article List         
Global API Exception Handling

Global API Exception Handling

Published on 16 Sep 2026     7 min read Web API
Web API

Global Exception Handling in ASP.NET Core Web API

Global exception handling is a centralized mechanism that catches unhandled exceptions from anywhere in the API and converts them into a consistent HTTP error response.

Instead of writing try-catch in every controller action, we configure one exception handler for the entire application.

ASP.NET Core provides built-in support through:

  • IExceptionHandler — recommended for .NET 8+

  • Exception-handling middleware

  • ProblemDetails — standard API error-response format

No external NuGet library is required.


1. Why do we need global exception handling?

Without global handling:

[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
    try
    {
        var product = await _service.GetByIdAsync(id);
        return Ok(product);
    }
    catch (Exception)
    {
        return StatusCode(500, "Something went wrong");
    }
}

This creates several problems:

  • Repeated try-catch blocks

  • Different error formats from different endpoints

  • Difficult logging and maintenance

  • Risk of exposing sensitive exception details

  • Controllers contain infrastructure concerns

With global handling:

[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
    var product = await _service.GetByIdAsync(id);
    return Ok(product);
}

If an exception occurs, the global handler catches it.


2. Request flow

flowchart TD
    A["Client request"] --> B["Exception middleware"]
    B --> C["Controller"]
    C --> D["Service"]
    D --> E["Repository / Database"]
    E -->|Success| F["Normal response"]
    E -->|Exception| B
    B --> G["Log exception"]
    G --> H["Map exception to status code"]
    H --> I["Return ProblemDetails JSON"]

Recommended implementation using IExceptionHandler

3. Create custom exceptions

Custom exceptions make it easy to distinguish different business errors.

NotFoundException.cs

namespace MyApi.Exceptions;

public sealed class NotFoundException : Exception
{
    public NotFoundException(string message)
        : base(message)
    {
    }
}

BadRequestException.cs

namespace MyApi.Exceptions;

public sealed class BadRequestException : Exception
{
    public BadRequestException(string message)
        : base(message)
    {
    }
}

ConflictException.cs

namespace MyApi.Exceptions;

public sealed class ConflictException : Exception
{
    public ConflictException(string message)
        : base(message)
    {
    }
}

4. Create the global exception handler

GlobalExceptionHandler.cs

using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using MyApi.Exceptions;

namespace MyApi.ExceptionHandlers;

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

    public GlobalExceptionHandler(
        ILogger<GlobalExceptionHandler> logger,
        IHostEnvironment environment)
    {
        _logger = logger;
        _environment = environment;
    }

    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        // 1. Log complete exception internally
        _logger.LogError(
            exception,
            "Unhandled exception occurred. TraceId: {TraceId}",
            httpContext.TraceIdentifier);

        // 2. Convert exception into an HTTP status code
        var statusCode = exception switch
        {
            BadRequestException     => StatusCodes.Status400BadRequest,
            UnauthorizedAccessException
                                    => StatusCodes.Status403Forbidden,
            NotFoundException       => StatusCodes.Status404NotFound,
            ConflictException       => StatusCodes.Status409Conflict,
            TimeoutException        => StatusCodes.Status504GatewayTimeout,
            _                       => StatusCodes.Status500InternalServerError
        };

        // 3. Create a standard ProblemDetails response
        var problemDetails = new ProblemDetails
        {
            Status = statusCode,
            Title = GetTitle(statusCode),

            // Do not expose internal exception details in production
            Detail = statusCode == StatusCodes.Status500InternalServerError
                ? _environment.IsDevelopment()
                    ? exception.Message
                    : "An unexpected error occurred. Please try again later."
                : exception.Message,

            Instance = httpContext.Request.Path
        };

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

        // 4. Return JSON response
        httpContext.Response.StatusCode = statusCode;

        await httpContext.Response.WriteAsJsonAsync(
            problemDetails,
            cancellationToken);

        // true means the exception has been handled
        return true;
    }

    private static string GetTitle(int statusCode)
    {
        return statusCode switch
        {
            StatusCodes.Status400BadRequest =>
                "Bad request",

            StatusCodes.Status403Forbidden =>
                "Access forbidden",

            StatusCodes.Status404NotFound =>
                "Resource not found",

            StatusCodes.Status409Conflict =>
                "Conflict occurred",

            StatusCodes.Status504GatewayTimeout =>
                "External service timeout",

            _ =>
                "Internal server error"
        };
    }
}

5. Register the exception handler

Program.cs

using MyApi.ExceptionHandlers;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Register standardized ProblemDetails support
builder.Services.AddProblemDetails();

// Register the global exception handler
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();

var app = builder.Build();

// Place it early so it can catch exceptions
// from middleware and endpoints registered after it.
app.UseExceptionHandler();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

The important registrations are:

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

app.UseExceptionHandler();

6. Service example

using MyApi.Exceptions;

public sealed class ProductService
{
    private static readonly List<Product> Products =
    [
        new Product(1, "Laptop", 75000),
        new Product(2, "Mobile", 35000)
    ];

    public Product GetById(int id)
    {
        if (id <= 0)
        {
            throw new BadRequestException(
                "Product ID must be greater than zero.");
        }

        var product = Products.FirstOrDefault(x => x.Id == id);

        if (product is null)
        {
            throw new NotFoundException(
                $"Product with ID {id} was not found.");
        }

        return product;
    }
}

public record Product(int Id, string Name, decimal Price);

7. Controller example

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public sealed class ProductsController : ControllerBase
{
    private readonly ProductService _productService;

    public ProductsController(ProductService productService)
    {
        _productService = productService;
    }

    [HttpGet("{id:int}")]
    public ActionResult<Product> GetById(int id)
    {
        var product = _productService.GetById(id);

        return Ok(product);
    }
}

Register the service:

builder.Services.AddScoped<ProductService>();

Notice that the controller does not contain a try-catch.


8. Example responses

Product not found

Request:

GET /api/products/100

Response:

HTTP/1.1 404 Not Found
Content-Type: application/problem+json
{
  "type": "about:blank",
  "title": "Resource not found",
  "status": 404,
  "detail": "Product with ID 100 was not found.",
  "instance": "/api/products/100",
  "traceId": "0HN7K2B8N98R2:00000001"
}

Unexpected exception

{
  "type": "about:blank",
  "title": "Internal server error",
  "status": 500,
  "detail": "An unexpected error occurred. Please try again later.",
  "instance": "/api/products/10",
  "traceId": "0HN7K2B8N98R2:00000002"
}

The client can provide the traceId to the support team. The support team can search the logs using that value.


9. Recommended exception-to-status-code mapping

Exception situation HTTP status
Invalid input 400 Bad Request
Authentication required 401 Unauthorized
Authenticated but no permission 403 Forbidden
Resource not found 404 Not Found
Duplicate record or concurrency conflict 409 Conflict
Business validation failed 422 Unprocessable Entity
External service timeout 504 Gateway Timeout
Unexpected programming/system error 500 Internal Server Error

Normally, authentication middleware produces 401, rather than throwing an exception.


10. What is ProblemDetails?

ProblemDetails provides a standard structure for API errors.

Important properties:

Property Purpose
Status HTTP status code
Title Short error description
Detail More information about the error
Instance API path where the error occurred
Type URI identifying the error type
Extensions Additional values such as traceId

Microsoft recommends Problem Details for consistent HTTP API error responses. Microsoft: Handle errors in ASP.NET Core APIs


11. Handle validation errors separately

Model validation errors are usually not exceptions.

With [ApiController], ASP.NET Core automatically returns 400 Bad Request when model validation fails.

public sealed class CreateProductRequest
{
    [Required]
    public string Name { get; set; } = string.Empty;

    [Range(1, 1_000_000)]
    public decimal Price { get; set; }
}

Controller:

[HttpPost]
public IActionResult Create(CreateProductRequest request)
{
    return Ok(request);
}

Invalid input automatically produces a validation response. Therefore, global exception handling should mainly handle unexpected exceptions and deliberately thrown business exceptions.


12. Middleware vs exception filter

Global exception middleware Exception filter
Handles errors across the application pipeline Handles MVC/controller exceptions
Can catch exceptions from middleware, controllers and services registered after it Mainly works inside the MVC pipeline
Recommended for general global handling Useful for controller-specific behaviour
Registered using UseExceptionHandler() Implements IExceptionFilter or IAsyncExceptionFilter

For application-wide handling, prefer exception-handling middleware with IExceptionHandler.


13. Important production practices

  • Never return stack traces, SQL queries, connection strings or internal class names.

  • Log the complete exception on the server.

  • Return a safe message to the client.

  • Include a traceId or correlation ID.

  • Use custom exceptions for known business situations.

  • Keep controller actions free from repetitive try-catch.

  • Do not convert every error to 500.

  • Do not use exceptions for normal control flow.

  • Ensure exception middleware is registered early.

  • Send logs to Serilog, Application Insights, Seq, Elasticsearch or another centralized system.

  • If the response has already started, the exception handler may be unable to replace it with an error response.

Important distinction

Global exception handling catches thrown exceptions. It does not automatically handle every unsuccessful status code, such as an ordinary 404 caused by an unknown URL. Status-code handling can be configured separately using UseStatusCodePages().


Interview-ready answer

Global exception handling is a centralized mechanism for catching unhandled exceptions throughout an ASP.NET Core Web API. In .NET 8+, we can implement IExceptionHandler, register it with AddExceptionHandler, and enable it using UseExceptionHandler. The handler logs the complete exception, maps known exceptions to appropriate HTTP status codes, and returns a consistent ProblemDetails response. This avoids repetitive try-catch blocks, prevents sensitive information from being exposed, and improves logging and maintainability.