Saltar al contenido principal

Normalizar errores

En el laburo, tengo un montón de escenarios como:

  • service.com/users/-1 devuelve:

    {
    "errorDescription": "User not found",
    "cause": "BAD REQUEST"
    }
  • pero service.com/product/-1 devuelve:

    {
    "message": "not found",
    "error": 404
    }

La consistencia en los errores se fue por la ventana, y empeora con errores dentro de respuestas 200 OK. Vos no querés ser ese tipo de dev: vamos a hacer un manejo de errores adecuado con @RestControllerAdvice y ProblemDetail.

Manejo de errores

@RestControllerAdvice actúa como un "coordinador central de errores" para tu aplicación.

  • Es un solo lugar donde podés definir cómo todos los errores, excepciones o escenarios inesperados se traducen en respuestas.
  • En lugar de dispersar la lógica de manejo de errores por cada controlador, esta herramienta asegura que cada error —ya sea por una búsqueda de usuario, producto o un bug interno— siga las mismas reglas y formato.

ProblemDetail es una "plantilla de error" estandarizada que estructura las respuestas de manera clara y consistente. Pensalo como un formulario prediseñado que cada error completa:

  • Qué tipo de error ocurrió (por ejemplo, "user_not_found").
  • Un título legible para humanos (por ejemplo, "Recurso no encontrado").
  • El código de estado HTTP (por ejemplo, 404).
  • Detalles adicionales (por ejemplo, "El ID de usuario -1 no existe").

Juntos, estas herramientas aseguran que tu microservicio nunca confunda a los clientes con formatos de error inconsistentes. Incluso los casos extremos o errores imprevistos se envuelven en la misma estructura predecible.

Las clases @RestControllerAdvice en la Arquitectura Hexagonal tienen sentido que estén en la carpeta /adapter/in. Sin embargo, podés encontrarlas en /config para indicar que es una preocupación transversal.

Creemos una clase @RestControllerAdvice.

src/main/java/dev/pollito/users_manager/config/advice/ControllerAdvice.java
package dev.pollito.users_manager.adapter.in.rest.advice;

import io.opentelemetry.api.trace.Span;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.resource.NoResourceFoundException;

@RestControllerAdvice
@Slf4j
public class ControllerAdvice {
@NotNull private static ProblemDetail buildProblemDetail(@NotNull Exception e, HttpStatus status) {
String exceptionSimpleName = e.getClass().getSimpleName();
log.error("{} being handled", exceptionSimpleName, e);
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(status, e.getLocalizedMessage());
problemDetail.setTitle(exceptionSimpleName);
problemDetail.setProperty("timestamp", DateTimeFormatter.ISO_INSTANT.format(Instant.now()));
problemDetail.setProperty("trace", Span.current().getSpanContext().getTraceId());
return problemDetail;
}

@ExceptionHandler(Exception.class)
public ProblemDetail handle(@NotNull Exception e) {
return buildProblemDetail(e, HttpStatus.INTERNAL_SERVER_ERROR);
}

@ExceptionHandler(NoResourceFoundException.class)
public ProblemDetail handle(@NotNull NoResourceFoundException e) {
return buildProblemDetail(e, HttpStatus.NOT_FOUND);
}
}

Si visitamos una uri que no existe (como http://localhost:8080), ahora vamos a obtener un error estandarizado:

error estándar

Agregando más manejadores

Ahora mismo podrías estar pensando

Pero "No static resource" debería ser 404 en lugar de 500.

A lo que yo te digo, sí, tenés toda la razón, y desearía que hubiera una forma de implementar ese comportamiento por defecto. Pero con esta normalización de errores, todo es un 500 a menos que digas explícitamente lo contrario. Creo que la compensación vale la pena.

Para que "No static resource" sea un 404, agregá en la clase @RestControllerAdvice un nuevo método @ExceptionHandler(NoResourceFoundException.class).

@ExceptionHandler(NoResourceFoundException.class)
public ProblemDetail handle(@NotNull NoResourceFoundException e) {
return buildProblemDetail(e, HttpStatus.NOT_FOUND);
}

Ahora, al pedir a http://localhost:8080 obtenemos el nuevo comportamiento esperado:

404 esperado

Commiteá el progreso hasta ahora.

git add .
git commit -m "manejo de errores"

Manejadores comunes que podrías necesitar

Acá tenés algunas excepciones comunes que quizás quieras manejar:

ExcepciónDescripciónEjemploNotas
ConstraintViolationExceptionLos parámetros/campos de la petición fallan la validación (@NotNull, @Size, @Pattern)Cuerpo de la petición sin un campo requeridoRequiere Jakarta EE (se agregará después)
MethodArgumentTypeMismatchExceptionEl parámetro de la petición no se puede convertir al tipo esperadoEl controlador espera un Integer pero recibe un String
NoResourceFoundExceptionLa petición accede a un recurso de Spring MVC que no existeAcceder a un endpoint no definido
NoSuchElementExceptionSe llama a Optional.get() en un Optional vacíoBuscar un usuario por ID que no existe
PropertyReferenceExceptionSe usa una propiedad inválida en una consulta de repositorio de Spring DataOrdenar por un campo que no existeRequiere Spring Data (se agregará después)