Saltar a contenido

Manejo de excepciones

Un mecanismo consistente para manejar excepciones evita duplicación, mejora la trazabilidad y traduce errores internos a respuestas HTTP comprensibles para los clientes.

Objetivos

  • Centralizar la conversión de excepciones a respuestas HTTP.
  • Mantener los controladores limpios y delegar el formato de errores a una capa común.
  • Registrar (log) errores y enviar la información mínima necesaria al cliente.

Herramientas en Spring

  • @ControllerAdvice: componente global que puede interceptar excepciones lanzadas por controladores.
  • @ExceptionHandler: indica el tipo de excepción a manejar y el método que genera la respuesta.
  • ResponseEntityExceptionHandler: clase base útil para manejar errores del framework (ej. MethodArgumentNotValidException).
  • @ResponseStatus (opcional): permite asociar un HttpStatus a una excepción concreta.

Patrón recomendado

  1. Definir un DTO de error (por ejemplo ErrorDto) que incluya: código, mensaje, timestamp y detalles opcionales.
  2. Implementar un @ControllerAdvice que capture excepciones conocidas y devuelva ResponseEntity<ErrorDto> con el HttpStatus adecuado.
  3. Registrar errores con niveles adecuados (WARN para validaciones, ERROR para fallos internos).

Ejemplo de ErrorDto

public class ErrorDto {
  private String code;
  private String message;
  private Instant timestamp = Instant.now();
  private List<String> details;
  // constructores, getters y setters
}

Ejemplo de @ControllerAdvice

@ControllerAdvice
public class ApiExceptionHandler extends ResponseEntityExceptionHandler {

  private final Logger log = LoggerFactory.getLogger(ApiExceptionHandler.class);

  @ExceptionHandler(EntityNotFoundException.class)
  public ResponseEntity<ErrorDto> handleNotFound(EntityNotFoundException ex) {
    log.debug("Recurso no encontrado: {}", ex.getMessage());
    ErrorDto err = new ErrorDto("NOT_FOUND", ex.getMessage(), List.of());
    return ResponseEntity.status(HttpStatus.NOT_FOUND).body(err);
  }

  @ExceptionHandler(BadRequestException.class)
  public ResponseEntity<ErrorDto> handleBadRequest(BadRequestException ex) {
    ErrorDto err = new ErrorDto("BAD_REQUEST", ex.getMessage(), List.of());
    return ResponseEntity.badRequest().body(err);
  }

  @Override
  protected ResponseEntity<Object> handleMethodArgumentNotValid(
      MethodArgumentNotValidException ex,
      HttpHeaders headers,
      HttpStatus status,
      WebRequest request) {
    List<String> details = ex.getBindingResult().getFieldErrors().stream()
        .map(e -> e.getField() + ": " + e.getDefaultMessage())
        .collect(Collectors.toList());
    ErrorDto err = new ErrorDto("VALIDATION_ERROR", "Campos inválidos", details);
    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(err);
  }

  @ExceptionHandler(Exception.class)
  public ResponseEntity<ErrorDto> handleAll(Exception ex) {
    log.error("Error inesperado", ex);
    ErrorDto err = new ErrorDto("INTERNAL_ERROR", "Ocurrió un error interno");
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(err);
  }
}

Consideraciones prácticas

  • No devolver trazas completas en producción; suficiente mensaje amigable y un identificador para el soporte.
  • Para APIs públicas, documentar los códigos de error y estructuras en la especificación (OpenAPI).
  • Distinguir entre errores de cliente (4xx) y del servidor (5xx).
  • Usar códigos de error internos (ej. USER_NOT_FOUND) para facilitar el manejo por parte del cliente.

¿Quieres que genere también un ejemplo de ErrorDto en un archivo separado y que lo referencie desde el ControllerAdvice?