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.