🧱 Lesson 13A: Centralized Error Handling & Validation Backend
Source: Dev.to

Series: From Code to Cloud: Building a Production-Ready .NET Application
By: Farrukh Rehman — Senior .NET Full Stack Developer / Team Lead
LinkedIn:
GitHub:
Source Code (Backend):
Source Code (Frontend):
🎯 Introduction
In this lesson we implement a Centralized Error Handling mechanism for our ASP.NET Core Web API. Instead of sprinkling try‑catch blocks throughout controller actions, we’ll use a Global Exception Middleware that:
- Catches all unhandled exceptions
- Logs them
- Returns a standardized JSON response to the client
Benefits:
- Consistency – All API errors share the same structure.
- Maintainability – Controllers stay focused on business logic.
- Security – Sensitive stack traces are hidden in production.
Step 1: Define the Standardized Error Response
Create a wrapper class that all error responses will use, ensuring 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
Define domain‑specific exceptions (independent of HTTP status codes). The middleware will later map them to appropriate HTTP codes.
NotFoundException
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.") { }
}
BadRequestException
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
The middleware sits in the HTTP pipeline, catches exceptions, determines the correct HTTP status code, and writes an ErrorResponse JSON payload.
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 _logger;
private readonly IHostEnvironment _env;
public ExceptionHandlingMiddleware(
RequestDelegate next,
ILogger 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
Add the middleware to the ASP.NET Core pipeline. The order matters – it should be registered early so it can catch errors from subsequent components.
File: ECommerce.API/Program.cs
// ... existing code ...
var app = builder.Build();
// Register the global exception handling middleware
app.UseMiddleware();
// ... other middleware registrations ...
app.Run();
Middleware Pipeline
// ------------------------------------------------------
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
// REGISTER MIDDLEWARE HERE
app.UseMiddleware();
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 GetProductById(Guid id)
{
var product = await _repository.GetByIdAsync(id);
if (product == null)
{
throw new NotFoundException(nameof(Product), id);
}
return _mapper.Map(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 an interceptor for error handling
- Implementing FluentValidation
- Maintaining consistent API responses
