Hoy vamos a crear un manejador de errores personalizado que te permitirá adaptar de forma flexible la lógica de tratamiento de excepciones a las necesidades de tu aplicación. Además, aprenderás a desarrollar tus propias excepciones y te asegurarás de que el usuario vea solo mensajes claros y útiles en lugar de extraños stack traces.
¿Por qué es importante?
Los errores son una parte inevitable de la programación. Pero la forma en que los manejas puede afectar mucho la experiencia del usuario y la estabilidad de la aplicación. Imagina, por ejemplo, que el usuario ve algo así:
org.springframework.dao.DataIntegrityViolationException: could not execute statement
Probablemente pensará: «¿Qué demonios? ¿Mi ordenador se rompió?!». Los manejadores de excepciones personalizados permiten convertir ese tipo de errores en mensajes claros, por ejemplo:
{
"error": "Invalid Request",
"message": "User with the same email already exists"
}
Mucho mejor, ¿no?
Paso 1: Crear una excepción personalizada
Antes de manejar los errores, vamos a crear una excepción propia. Debe ser lo bastante específica para que podamos distinguirla fácilmente de otros tipos de errores.
Ejemplo: UserNotFoundException
// Esta es nuestra excepción personalizada
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
Fíjate que la excepción extiende RuntimeException. Eso significa que es unchecked (no verificada) y no requiere manejo obligatorio en el código donde puede lanzarse, lo que la hace más adecuada para la mayoría de casos en aplicaciones web.
¿Dónde lanzar la excepción?
Supongamos que tenemos un servicio para trabajar con usuarios:
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User findUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User with id " + id + " not found"));
}
}
Si el usuario con el identificador indicado no se encuentra, nuestro servicio lanzará UserNotFoundException.
Paso 2: Crear un manejador de errores personalizado
Ahora queremos interceptar UserNotFoundException y convertirla en una respuesta más cómoda para el cliente (por ejemplo, JSON con un mensaje de error).
Para ello usamos un manejador global de errores con @ControllerAdvice.
Implementación del manejador personalizado
@RestControllerAdvice
public class CustomExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND) // HTTP 404
public ErrorResponse handleUserNotFoundException(UserNotFoundException ex) {
return new ErrorResponse("Not Found", ex.getMessage());
}
}
Clase auxiliar ErrorResponse
Para devolver una respuesta comprensible al cliente, creamos un DTO:
public class ErrorResponse {
private String error;
private String message;
public ErrorResponse(String error, String message) {
this.error = error;
this.message = message;
}
// Getters y setters para la serialización
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
Paso 3: Probar el manejador de errores personalizado
Ahora, para comprobar cómo funciona todo esto, añadamos un controlador:
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
return userService.findUserById(id);
}
}
Ejemplo de petición:
GET /users/42
Si el usuario con ID 42 no existe, el servidor devolverá:
{
"error": "Not Found",
"message": "User with id 42 not found"
}
Paso 4: Registro de errores
No solo es importante devolver respuestas correctas al cliente, sino también registrar información detallada para los desarrolladores. Añadimos logging en nuestro manejador:
@RestControllerAdvice
public class CustomExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(CustomExceptionHandler.class);
@ExceptionHandler(UserNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleUserNotFoundException(UserNotFoundException ex) {
logger.error("User not found exception: {}", ex.getMessage());
return new ErrorResponse("Not Found", ex.getMessage());
}
}
Ahora, cada vez que ocurra UserNotFoundException, el error se registrará en los logs.
Paso 5: Manejo de múltiples excepciones
A veces hace falta manejar varios tipos de excepciones a la vez. Simplemente añade más métodos en CustomExceptionHandler con diferentes @ExceptionHandler.
Ejemplo de manejo de IllegalArgumentException
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST) // HTTP 400
public ErrorResponse handleIllegalArgumentException(IllegalArgumentException ex) {
return new ErrorResponse("Bad Request", ex.getMessage());
}
Ahora, si en alguna parte de la aplicación se lanza IllegalArgumentException, el cliente recibirá:
{
"error": "Bad Request",
"message": "Invalid input data"
}
Paso 6: Probar los manejadores de errores
Para probar los manejadores usa herramientas como Postman o cURL, o escribe tests de integración.
Ejemplo de test con MockMvc
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void shouldReturn404WhenUserNotFound() throws Exception {
Mockito.when(userService.findUserById(42L))
.thenThrow(new UserNotFoundException("User with id 42 not found"));
mockMvc.perform(get("/users/42"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.error").value("Not Found"))
.andExpect(jsonPath("$.message").value("User with id 42 not found"));
}
}
Aplicación práctica
En proyectos reales, los manejadores de errores personalizados se usan para:
- Evitar filtrar detalles técnicos (por ejemplo, stack traces) al cliente.
- Asegurar uniformidad en el manejo de errores y el formato de las respuestas.
- Separar claramente la responsabilidad del manejo de errores entre desarrolladores (distintos controllers pueden tener sus propios manejadores personalizados).
- Facilitar el logging y el rastreo de las causas de los errores.
Ahora tu aplicación está lista para manejar todo tipo de excepciones, y los usuarios te lo agradecerán por los mensajes amigables en vez de los enigmáticos "500 Internal Server Error".
GO TO FULL VERSION