Error handling is a fundamental part of building reliable APIs. In this post, Iโll walk you through how to implement global error handling in ASP.NET Core using custom middleware โ with clean JSON error responses and Serilog for structured logging.
๐ง Why Use Global Error Handling?
Instead of wrapping every controller action in a try-catch, we can centralize error handling using middleware. This gives us:
โ
Clean, consistent error responses
โ
Less duplicated code
โ
Easy error tracing with unique IDs
โ
Integration with logging libraries like Serilog
๐ง Step 1: Create the Middleware Class
using System.Net; namespace MyApp.Middlewares { public class ExceptionHandlerMiddleware { private readonly ILogger<ExceptionHandlerMiddleware> _logger; private readonly RequestDelegate _next; public ExceptionHandlerMiddleware(ILogger<ExceptionHandlerMiddleware> logger, RequestDelegate next) { _logger = logger; _next = next; } public async Task InvokeAsync(HttpContext httpContext) { try { await _next(httpContext); } catch (Exception ex) { var errorId = Guid.NewGuid(); // Log the error with a unique identifier _logger.LogError(ex, $"[{errorId}] Unhandled exception: {ex.Message}"); httpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; httpContext.Response.ContentType = "application/json"; var error = new { Id = errorId, ErrorMessage = "Something went wrong. Please contact support with the error ID." }; await httpContext.Response.WriteAsJsonAsync(error); } } } }
๐ This middleware:
- Catches any unhandled exceptions
- Logs them with a unique Guid
- Returns a friendly error message in JSON format
โ๏ธ Step 2: Register the Middleware in Program.cs
Make sure it's registered early in the request pipeline:
var app = builder.Build(); // Add our global exception handler middleware app.UseMiddleware<ExceptionHandlerMiddleware>(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers();
๐งช Step 3: Throw an Error in a Controller to Test
[HttpGet] public IActionResult Crash() { throw new Exception("Simulated crash!"); }
Call this endpoint. You should see:
- A clean JSON response with an error ID
- An error logged in console/file (if Serilog is configured)
โ๏ธ Bonus: Integrate Serilog for Logging
Add Serilog via NuGet:
dotnet add package Serilog.AspNetCore dotnet add package Serilog.Sinks.Console dotnet add package Serilog.Sinks.File
Then, update your Program.cs
:
using Serilog; Log.Logger = new LoggerConfiguration() .MinimumLevel.Information() .WriteTo.Console() .WriteTo.File("Logs/log.txt", rollingInterval: RollingInterval.Day) .CreateLogger(); builder.Logging.ClearProviders(); builder.Logging.AddSerilog();
Now all _logger.LogError()
calls in your middleware (and anywhere else) will log to both the console and Logs/log.txt
.
๐งผ Best Practices
- โ Keep your middleware early in the pipeline.
- โ Always return consistent JSON error structures.
- โ Add contextual info like UserId, RequestPath, etc., if needed.
- ๐ Consider logging HTTP context data (cautiously) in production.
- ๐ก Use log enrichment with Serilog for deeper insights.
โ
Final Thoughts
Global exception handling with custom middleware is a clean and scalable way to handle API errors. It improves developer experience, user experience, and observability โ especially when paired with Serilog.
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.