Global Error Handling
Never show NullReferenceException to users again. Return clean JSON errors.
The Problem: Controller throws exception โ ASP.NET returns HTML page with full stack trace. React app crashes trying to
JSON.parse() it. Users see "Something went wrong". You have no logs.1. The Zomato Standard - What Errors Should Look Like
Bad: 500 Internal Server Error + HTML with at Zomato.API.Controllers.OrdersController.Get()
Good: 500 Internal Server Error + JSON:
{
"error": {
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred. Please try again.",
"traceId": "0HMVFE0A284AM:00000001"
}
}
Why: Frontend can display message. You can search logs using traceId. No sensitive data leaked.
2. Solution: Global Exception Middleware
Catches ANY unhandled exception from controllers, filters, services.
// Middleware/GlobalExceptionHandler.cs
public class GlobalExceptionHandler : IExceptionHandler {
private readonly ILogger _logger;
public GlobalExceptionHandler(ILogger logger) => _logger = logger;
public async ValueTask TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken) {
var traceId = Activity.Current?.Id?? httpContext.TraceIdentifier;
_logger.LogError(exception, "Unhandled exception. TraceId: {TraceId}", traceId);
var problemDetails = new ProblemDetails {
Status = StatusCodes.Status500InternalServerError,
Title = "Server Error",
Detail = "An unexpected error occurred. Please try again.",
Extensions = { ["traceId"] = traceId }
};
httpContext.Response.StatusCode = problemDetails.Status.Value;
await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
return true; // Mark as handled
}
}
// Program.cs -.NET 8 way
builder.Services.AddExceptionHandler();
builder.Services.AddProblemDetails(); // Standard RFC 7807 format
var app = builder.Build();
app.UseExceptionHandler(); // Must be first! Catches all errors
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
3. Handle Known Errors Differently
Not all exceptions = 500. Custom exceptions = clean codes.
// Exceptions/NotFoundException.cs
public class NotFoundException : Exception {
public NotFoundException(string message) : base(message) {}
}
// GlobalExceptionHandler.cs - update TryHandleAsync
public async ValueTask TryHandleAsync(...) {
var statusCode = exception switch {
NotFoundException => StatusCodes.Status404NotFound,
UnauthorizedAccessException => StatusCodes.Status401Unauthorized,
ArgumentException => StatusCodes.Status400BadRequest,
_ => StatusCodes.Status500InternalServerError
};
var message = statusCode == 500
? "An unexpected error occurred"
: exception.Message; // Safe to show for known errors
var problemDetails = new ProblemDetails {
Status = statusCode,
Title = GetTitle(statusCode),
Detail = message,
Extensions = { ["traceId"] = traceId }
};
...
}
Controller: throw new NotFoundException("Restaurant not found") โ 404 JSON, not 500.
Career-Killer Mistake: Returning
Fix: Only show
exception.Message for all errors. new Exception("Connection string: Server=prod-db;Pwd=Secret123") โ You just leaked DB password to hackers. Fix: Only show
exception.Message for custom exceptions you control. For Exception or SqlException, return generic "Server Error". Log the real message server-side.
Quick Check ๐ง
Error handling done. Next: Async & CancellationToken - Make API handle 10,000 users at once without freezing. Cancel DB queries when user closes browser. This is the performance layer. Continue โ
No comments yet. Be the first to share your thoughts!