
Global Exception Handling in ASP.NET WEB API
Author - Abdul Rahman (Bhai)
Web API
23 Articles
Table of Contents
What we gonna do?
In this article, let's learn about Global Exception Handling in Web API in ASP.NET Core.
Why we gonna do?
Global Exception Handling allows us to handle exceptions globally in one single place inside the application rather than scattering try-catch blocks everywhere in the code base. This is a good and clean practice that simplifies code maintenance and improves readability.
Errors are inevitable in APIs. To standardize error responses, ASP.NET Core supports the Problem Details payload as defined by RFC 7807. By enabling the default implementation of IProblemDetailsService through AddProblemDetails, we can automatically generate structured error responses. This approach integrates seamlessly with middlewares like the exception handler and developer exception page, providing clean and consistent error details in both production and development environments. This simplifies debugging and enhances the API's usability.
How we gonna do?
To implement Global Exception Handling, follow these steps:
Starting .NET 8, We can achieve this using the AddProblemDetails to Get structured error response defined in RFC 7807 standard.
DomainException and UnauthorizedException are application-specific exception classes you define — they represent known, expected failure conditions that deserve a distinct, meaningful error response. Only well-known exception types should expose their message to callers; all others fall back to a safe generic message.
using Microsoft.AspNetCore.Diagnostics; using Microsoft.EntityFrameworkCore; // DbUpdateException var builder = WebApplication.CreateBuilder(args); builder.Services .AddProblemDetails(options => options.CustomizeProblemDetails = (ctx) => { var hostEnv = ctx.HttpContext.RequestServices.GetRequiredService<IHostEnvironment>(); var logger = ctx.HttpContext.RequestServices.GetRequiredService<ILogger<ProblemDetails>>(); // Populated by UseExceptionHandler() — null if no exception was thrown var exception = ctx.HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error; if (ctx.ProblemDetails.Status == 500) { if (exception is null) return; // non-exception 500s are left as-is logger.LogCritical(exception, "Unhandled exception of type {ExceptionType}", exception.GetType().Name); if (exception is DomainException domainEx) { // DomainException is assumed to carry only user-safe messages by design; // add a hostEnv.IsDevelopment() guard here if yours could contain sensitive internals ctx.ProblemDetails.Title = "An error occurred in our API. Use the trace id when contacting us."; ctx.ProblemDetails.Detail = domainEx.Message; } else if (exception is UnauthorizedException) { ctx.ProblemDetails.Title = "An error occurred in our API. Use the trace id when contacting us."; ctx.ProblemDetails.Detail = "User is not authorized to perform this action."; } else if (exception is DbUpdateException) { ctx.ProblemDetails.Title = "An error occurred in our API. Use the trace id when contacting us."; ctx.ProblemDetails.Detail = "An error occurred while saving data. Please try again later."; } else if (exception is Exception ex) { ctx.ProblemDetails.Title = "An unexpected fault happened. Try again later."; ctx.ProblemDetails.Detail = hostEnv.IsDevelopment() ? ex.Message : "An unexpected fault happened. Try again later."; } } } )Register the middleware as the first middleware in the pipeline:
var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.UseExceptionHandler() .UseRouting() .UseEndpoints(endpoints => { endpoints.MapControllers(); });; app.Run();The alternative is to Create a Global Exception Middleware as shown below if you want a custom response format implementation:
using System.Net; namespace ILoveDotNet.Middlewares; public sealed class ExceptionHandlerMiddleware : IMiddleware { private readonly IWebHostEnvironment _env; private readonly ILogger<ExceptionHandlerMiddleware> _logger; public ExceptionHandlerMiddleware(IWebHostEnvironment env, ILogger<ExceptionHandlerMiddleware> logger) { _env = env; _logger = logger; } public async Task InvokeAsync(HttpContext context, RequestDelegate next) { try { await next(context); } catch (DomainException ex) { await HandleDomainExceptionAsync(context, ex); } catch (Exception ex) { await HandleExceptionAsync(context, ex); } } private Task HandleDomainExceptionAsync(HttpContext context, DomainException exception) { _logger.LogCritical(exception, "Unhandled domain exception of type {ExceptionType}", exception.GetType().Name); string result = exception.Message; context.Response.ContentType = MediaTypeNames.Application.Json; context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; return context.Response.WriteAsync(result); } private Task HandleExceptionAsync(HttpContext context, Exception exception) { _logger.LogCritical(exception, "Unhandled exception of type {ExceptionType}", exception.GetType().Name); var result = _env.IsProduction() ? "An unexpected fault happened. Try again later." : exception.Message; context.Response.ContentType = MediaTypeNames.Application.Json; context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; return context.Response.WriteAsync(result); } } public static class ExceptionHandlerMiddlewareExtensions { public static IApplicationBuilder UseGlobalExceptionHandler(this IApplicationBuilder builder) { return builder.UseMiddleware<ExceptionHandlerMiddleware>(); } }This middleware inherits from IMiddleware and catches exceptions thrown from the application. It writes a general error message in production and the actual error message in non-production environments.
Register the middleware as the first middleware in the pipeline:
var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.UseGlobalExceptionHandler() .UseRouting() .UseEndpoints(endpoints => { endpoints.MapControllers(); });; app.Run();Registering it first ensures that all exceptions are caught. If registered later, some exceptions might be missed.
Summary
In this article, we explored two approaches to handle exceptions globally in ASP.NET Web API. The recommended approach uses AddProblemDetails with a CustomizeProblemDetails callback to produce typed, RFC 7807-compliant error responses — mapping DomainException, UnauthorizedException, DbUpdateException, and generic exceptions to distinct, safe error bodies. The alternative uses a custom IMiddleware for full control over the response format. Both approaches centralise exception handling, eliminate scattered try-catch blocks, and protect callers from internal implementation details.
Once exceptions are handled globally, the next step is replacing the default console logger with a production-grade solution. See Structured Logging with Serilog in ASP.NET Web API to add enriched, environment-aware logging alongside the exception handler you just built.