🧱 Lesson 13A: Centralized Error Handling & Validation Backend

Published: (February 16, 2026 at 12:39 AM EST)
4 min read
Source: Dev.to

Source: Dev.to

Cover image for 🧱 Lesson 13A: Centralized Error Handling & Validation Backend

Farrukh Rehman

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
0 views
Back to Blog

Related posts

Read more »

A Full-Stack Productivity Platform

I created Focus Portal to help people stay focused and organized in a world full of distractions. I wanted a simple platform where users could manage tasks, wri...