El manejo global de excepciones es una forma de capturar errores en un único punto del pipeline.
Hay un olor en el código que suele indicar que algo no está bien estructurado: el exceso de try-catch.
Abres un controlador y ves esto:
// ❌ EL ANTI-PATRÓN: Programación defensiva paranoica
[HttpGet("{id}")]
public IActionResult Get(int id)
{
try
{
var item = _service.Get(id);
return Ok(item);
}
catch (KeyNotFoundException ex)
{
return NotFound(ex.Message);
}
catch (ArgumentException ex)
{
return BadRequest(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error grave");
return StatusCode(500, "Algo explotó");
}
}Si tienes 50 endpoints, ¿vas a copiar y pegar ese bloque 50 veces? Si decides cambiar el formato del error 500, tendrás que editar 50 archivos.
En ASP.NET Core, la filosofía es distinta: deja que la excepción suba hasta un manejador global.
Es decir, dejamos que la excepción ocurra y “suba” (burbujee). Nosotros pondremos una red de seguridad global en el pipeline que capturará cualquier error, lo registrará y devolverá una respuesta consistente.
Middleware de excepciones
ASP.NET Core tiene un middleware nativo diseñado para esto: UseExceptionHandler.
Cuando una excepción ocurre en tu controlador (o servicio, o repositorio), sube por el pipeline. Si nadie la captura, el servidor devuelve un error; con este middleware podemos interceptarla y generar una respuesta controlada.
IExceptionHandler en .NET 8+
Hasta hace poco, configurar esto requería escribir un middleware personalizado a mano. Desde .NET 8, tenemos una interfaz maravillosa: IExceptionHandler.
Vamos a crear una clase que centralice toda nuestra lógica de errores.
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
public class GlobalExceptionHandler : IExceptionHandler
{
private readonly ILogger<GlobalExceptionHandler> _logger;
public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
{
_logger = logger;
}
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
_logger.LogError(exception, "Ocurrió un error no controlado: {Message}", exception.Message);
// Definimos la respuesta base
var problemDetails = new ProblemDetails
{
Title = "Ocurrió un error inesperado",
Instance = httpContext.Request.Path
};
// Personalizamos según el tipo de excepción (Pattern Matching)
switch (exception)
{
case ArgumentException:
problemDetails.Status = StatusCodes.Status400BadRequest;
problemDetails.Title = "Error de Validación";
problemDetails.Detail = exception.Message;
break;
case KeyNotFoundException:
problemDetails.Status = StatusCodes.Status404NotFound;
problemDetails.Title = "Recurso no encontrado";
problemDetails.Detail = exception.Message;
break;
case UnauthorizedAccessException:
problemDetails.Status = StatusCodes.Status403Forbidden;
problemDetails.Title = "Acceso Denegado";
break;
default:
problemDetails.Status = StatusCodes.Status500InternalServerError;
problemDetails.Title = "Error Interno del Servidor";
problemDetails.Detail = "Se ha producido un error inesperado.";
break;
}
httpContext.Response.StatusCode = problemDetails.Status.Value;
// Escribimos la respuesta como JSON
await httpContext.Response
.WriteAsJsonAsync(problemDetails, cancellationToken);
// Retornamos true para decir: "Yo me he encargado, no sigas propagando el error"
return true;
}
}¿Qué es ProblemDetails?
Es el formato estándar de Problem Details para devolver errores en APIs HTTP (RFC 9457, anteriormente RFC 7807). En lugar de inventarnos un JSON {"error": "malo"}, usamos un formato conocido por herramientas y clientes.
Registrar el handler en Program.cs
Ahora tenemos que decirle a .NET que use nuestra clase.
var builder = WebApplication.CreateBuilder(args);
// 1. Registramos nuestra clase en la DI
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
// 2. Añadimos detalles de problemas estándar
builder.Services.AddProblemDetails();
var app = builder.Build();
// 3. ¡IMPORTANTE! Activamos el middleware
app.UseExceptionHandler();
app.MapControllers();
app.Run();El orden importa: app.UseExceptionHandler() debe estar al principio del pipeline, antes de los componentes cuyos errores queremos capturar.
Cómo quedan los controladores
Después de implementar esto, volvemos a nuestro controlador paranoico del principio y lo limpiamos.
// ✅ BIEN: Limpio, enfocado y legible
[HttpGet("{id}")]
public IActionResult Get(int id)
{
// Si _service lanza excepción, el GlobalHandler se encarga.
// Nosotros solo programamos el "camino feliz".
var item = _service.Get(id);
return Ok(item);
}Fíjate en la diferencia. El código se reduce un 70% y es mucho más fácil de leer.
Excepciones de dominio
Para distinguir errores de negocio, podemos crear excepciones personalizadas en la capa de Domain.
// En MiApp.Domain
public class ProductoAgotadoException : Exception
{
public ProductoAgotadoException(int id)
: base($"El producto {id} no tiene stock.") { }
}Luego, en nuestro GlobalExceptionHandler, añadimos un case para ella:
case ProductoAgotadoException:
problemDetails.Status = StatusCodes.Status409Conflict; // Conflict
problemDetails.Title = "Stock Insuficiente";
break;De esta forma, tu lógica de negocio (Servicios/Dominio) no tiene que saber nada de HTTP ni códigos 400/500. Simplemente dice “¡Algo va mal!” lanzando una excepción semántica, y la capa de API (el Handler) decide cómo traducir eso al idioma web.