A global exception handler in ASP.NET Core provides a centralized mechanism to catch and process unhandled exceptions that occur anywhere in your application's request pipeline. This ensures that your application can gracefully handle errors, log them effectively, and return consistent, user-friendly error responses to clients, rather than exposing raw server errors or crashing.
1. GlobalExceptionHandlerMiddleware.cs
This C# class defines the custom middleware. It implements the `IMiddleware` interface, which is the recommended way to create middleware in modern ASP.NET Core, allowing for dependency injection.
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Hosting;
using System;
using System.Net;
using System.Text.Json;
using System.Threading.Tasks;
namespace YourAppName.Middleware
{
// This class defines our custom exception handling middleware.
// It implements the IMiddleware interface, which is a modern way to create middleware
// in ASP.NET Core without requiring extension methods.
public class GlobalExceptionHandlerMiddleware : IMiddleware
{
private readonly ILogger<GlobalExceptionHandlerMiddleware> _logger;
private readonly IHostEnvironment _env; // To check environment (Development/Production)
// Constructor for dependency injection.
// ILogger is used for logging errors, and IHostEnvironment is used to determine
// if we are in a development or production environment.
public GlobalExceptionHandlerMiddleware(ILogger<GlobalExceptionHandlerMiddleware> logger, IHostEnvironment env)
{
_logger = logger;
_env = env;
}
// The InvokeAsync method is the core of the middleware.
// It processes the HTTP request.
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
try
{
// Call the next middleware in the pipeline.
// If an exception occurs in any subsequent middleware or controller,
// it will be caught by the catch block below.
await next(context);
}
catch (Exception ex)
{
// Log the exception details.
// In a real application, you would use structured logging (e.g., Serilog, NLog)
// and potentially send this to a centralized logging system (e.g., ELK stack, Azure Application Insights).
_logger.LogError(ex, "An unhandled exception occurred during request processing: {ErrorMessage}", ex.Message);
// Set the HTTP response status code to Internal Server Error (500).
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
context.Response.ContentType = "application/json"; // Ensure response is JSON
// Create a standardized error response object.
var errorResponse = new ErrorResponse
{
StatusCode = context.Response.StatusCode,
Message = "An unexpected error occurred. Please try again later.", // Generic message for production
TraceId = context.TraceIdentifier // Unique ID for correlating logs
};
// In development, include more details for debugging.
// NEVER expose sensitive error details (like stack traces) in production.
if (_env.IsDevelopment())
{
errorResponse.Message = ex.Message; // Show specific error message
errorResponse.Detailed = ex.StackTrace; // Include stack trace
}
// Serialize the error response to JSON and write it to the HTTP response body.
await context.Response.WriteAsync(JsonSerializer.Serialize(errorResponse, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase // Ensures JSON properties are camelCase
}));
}
}
}
// Helper class to define the structure of our error response.
public class ErrorResponse
{
public int StatusCode { get; set; }
public string Message { get; set; }
public string TraceId { get; set; }
public string Detailed { get; set; } // Only for development environment
}
// Extension method to easily register the middleware in Program.cs.
public static class GlobalExceptionHandlerMiddlewareExtensions
{
public static IApplicationBuilder UseGlobalExceptionHandler(this IApplicationBuilder builder)
{
// Use the middleware. The order of middleware registration is crucial.
// This middleware should typically be registered very early in the pipeline
// so it can catch exceptions from subsequent middleware and controllers.
return builder.UseMiddleware<GlobalExceptionHandlerMiddleware>();
}
}
}
2. Registering the Middleware in Program.cs (or Startup.cs)
The middleware needs to be registered in your application's startup file. Its placement in the request pipeline is critical.
For .NET 6+ (Minimal APIs - Program.cs):
using YourAppName.Middleware; // Make sure to add this using directive
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Register your custom middleware as a scoped service.
// This allows dependency injection into the middleware's constructor.
builder.Services.AddScoped<GlobalExceptionHandlerMiddleware>();
var app = builder.Build();
// Configure the HTTP request pipeline.
// IMPORTANT: Place this middleware VERY EARLY in the pipeline.
// It should be before any other middleware that might throw unhandled exceptions
// that you want to catch globally (e.g., before app.UseRouting(), app.UseAuthorization(), etc.).
app.UseGlobalExceptionHandler(); // Use our custom extension method
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
For .NET 5 and earlier (Startup.cs):
using YourAppName.Middleware; // Make sure to add this using directive
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace YourAppName
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
// Register your custom middleware as a scoped service
services.AddScoped<GlobalExceptionHandlerMiddleware>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// IMPORTANT: Place this middleware VERY EARLY in the pipeline.
// It should be before any other middleware that might throw unhandled exceptions.
app.UseGlobalExceptionHandler(); // Use our custom extension method
if (env.IsDevelopment())
{
// You might remove or comment out app.UseDeveloperExceptionPage()
// if you want your custom handler to always be active,
// even in development, for consistent testing.
// app.UseDeveloperExceptionPage();
}
else
{
// In production, you might want to use a more generic error page
// or redirect to a specific error route.
// app.UseExceptionHandler("/Error");
// The custom middleware handles the API error response.
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
Key Considerations
-
Middleware Order: The placement of `app.UseGlobalExceptionHandler();` is critical. It should be placed very early in the pipeline to ensure it can catch exceptions from almost all subsequent middleware and your application's core logic.
-
Specific vs. Global Handling: This global handler is for unhandled exceptions. For expected errors (e.g., invalid input, resource not found), it's often better to handle them closer to where they occur (e.g., using `if` checks, custom exceptions, or `ValidationFilterAttribute` in controllers) to return more specific HTTP status codes and detailed error messages. The global handler acts as a fallback for anything unexpected.
-
Logging: Integrate with a robust logging framework (like Serilog, NLog, or built-in `ILogger` with a good provider) to ensure errors are persistently stored and easily searchable.
-
Security: Always be mindful of what information you expose to the client, especially in production. Never expose sensitive error details (like database errors, full stack traces, or internal system paths) to the client in a production environment.
-
`UseDeveloperExceptionPage()` / `UseExceptionHandler()`: If you use `app.UseDeveloperExceptionPage()` (in development) or `app.UseExceptionHandler()` (in production) in `Program.cs`/`Startup.cs`, they might intercept exceptions before your custom middleware does. For your custom middleware to take full control, you might need to remove or strategically place these built-in handlers.
By implementing this global exception handler middleware, you significantly enhance the robustness, maintainability, and user experience of your ASP.NET Core application.
Comments - Beta - WIP