Series: From Code to Cloud: Building a Production-Ready .NET Application
By: Farrukh Rehman - Senior .NET Full Stack Developer / Team Lead
LinkedIn: https://linkedin.com/in/farrukh-rehman
GitHub: https://github.com/farrukh1212cs
Source Code Backend : https://github.com/farrukh1212cs/ECommerce-Backend.git
Source Code Frontend : https://github.com/farrukh1212cs/ECommerce-Frontend.git
🎯 Introduction
In this lesson, we implement a Centralized Error Handling mechanism for our ASP.NET Core Web API. Instead of using try-catch blocks in every controller action, we will use a Global Exception Middleware. This middleware will catch all unhandled exceptions, log them, and return a standardized JSON response to the client.
This approach ensures:
- Consistency: All API errors follow the same structure.
- Maintainability: Controllers remain clean and focused on business logic.
- Security: We avoid leaking sensitive stack traces in production.
Step 1: Define the Standardized Error Response
First, we define a wrapper class that all error responses will use. This ensures the frontend always knows what format to expect.
File: ECommerce.Application/Common/ErrorResponse.cs
namespace ECommerce.Application.Common;
public class ErrorResponse
{
public int StatusCode { get; set; }
public string Message { get; set; } = string.Empty;
public object? Details { get; set; }
public ErrorResponse(int statusCode, string message, object? details = null)
{
StatusCode = statusCode;
Message = message;
Details = details;
}
}
Step 2: Create Custom Domain Exceptions
We create custom exceptions in the Domain layer. These exceptions represent specific business scenarios (e.g., "Resource Not Found") and are independent of HTTP status codes. The middleware will map these to HTTP codes later.
File: ECommerce.Domain/Exceptions/NotFoundException.cs
namespace ECommerce.Domain.Exceptions;
public class NotFoundException : Exception
{
public NotFoundException(string message) : base(message)
{
}
public NotFoundException(string name, object key)
: base($"Entity \"{name}\" ({key}) was not found.")
{
}
}
File: ECommerce.Domain/Exceptions/BadRequestException.cs
namespace ECommerce.Domain.Exceptions;
public class BadRequestException : Exception
{
public BadRequestException(string message) : base(message)
{
}
}
Step 3: Implement the Global Middleware
This is the core component. It sits in the HTTP pipeline, catches exceptions, determines the appropriate HTTP status code, and writes the
ErrorResponse
JSON.
File: ECommerce.API/Middleware/ExceptionHandlingMiddleware.cs
using System.Net;
using System.Text.Json;
using ECommerce.Application.Common;
using ECommerce.Domain.Exceptions;
namespace ECommerce.API.Middleware;
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
private readonly IHostEnvironment _env;
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger, IHostEnvironment env)
{
_next = next;
_logger = logger;
_env = env;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
_logger.LogError(exception, "An unhandled exception has occurred: {Message}", exception.Message);
context.Response.ContentType = "application/json";
// Map specific exceptions to HTTP Status Codes
var response = exception switch
{
NotFoundException => new ErrorResponse((int)HttpStatusCode.NotFound, exception.Message),
BadRequestException => new ErrorResponse((int)HttpStatusCode.BadRequest, exception.Message),
_ => new ErrorResponse((int)HttpStatusCode.InternalServerError, "An internal server error has occurred.", _env.IsDevelopment() ? exception.StackTrace : null)
};
context.Response.StatusCode = response.StatusCode;
var json = JsonSerializer.Serialize(response, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
await context.Response.WriteAsync(json);
}
}
Step 4: Register the Middleware
Finally, we tell ASP.NET Core to use our middleware. The order matters! It should be registered early in the pipeline to catch errors from other middleware.
File: ECommerce.API/Program.cs
// ... existing code ...
var app = builder.Build();
// ------------------------------------------------------
// Middleware Pipeline
// ------------------------------------------------------
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
// REGISTER MIDDLEWARE HERE
app.UseMiddleware<ECommerce.API.Middleware.ExceptionHandlingMiddleware>();
app.UseHttpsRedirection();
// ... rest of the pipeline
Step 5: Usage & Verification
Now, you can throw exceptions from anywhere in your Application or Domain layers, and they will be automatically handled.
Usage Example:
public async Task<ProductDto> GetProductById(Guid id)
{
var product = await _repository.GetByIdAsync(id);
if (product == null)
{
throw new NotFoundException(nameof(Product), id);
}
return _mapper.Map<ProductDto>(product);
}
Response Example (404 Not Found):
{
"statusCode": 404,
"message": "Entity \"Product\" (d290f1ee-6c54-4b01-90e6-d701748f0851) was not found.",
"details": null
}
Next Lecture Preview
Lecture 13B : Centralized Error Handling & Validation Frontend
Using interceptor for error handling, implementing FluentValidation, and maintaining consistent API responses.
Top comments (1)
This covers one of the core backend practices that separate maintainable systems from quick hacks.
One thing that helped my teams is pairing centralized error handling with standardized error responses — that way frontends / API consumers can consistently handle failures without guessing.
Do you have a preferred pattern for integrating validation frameworks with custom middleware?