Empecemos con una pregunta sencilla: ¿qué pasa si tu aplicación se topa con algo inesperado? Por ejemplo, el usuario pedirá un recurso que no existe, o en vez de un número introduce una cadena. Sin un manejo adecuado, tu servidor orgullosamente vomitará en el navegador un bonito stacktrace con un error 500, que, lamentablemente, no hará feliz al usuario.
El manejo de excepciones ayuda a:
- Mostrar al usuario mensajes comprensibles en vez de "algo salió mal".
- Centralizar el control de errores.
- Minimizar el caos en el código de los controladores.
- Mejorar la seguridad (por ejemplo, evitar la fuga de información técnica).
@ExceptionHandler: tu superhéroe para el manejo local de errores
Spring MVC ofrece la anotación @ExceptionHandler, que permite manejar excepciones que ocurren en los controladores. Así es como funciona.
Manejo simple de un error
@Controller
public class DemoController {
@GetMapping("/hello")
public String hello(@RequestParam(required = false) String name) {
if (name == null) {
throw new IllegalArgumentException("¡El nombre es obligatorio!");
}
return "¡Hola, " + name;
}
@ExceptionHandler(IllegalArgumentException.class)
@ResponseBody
public String handleIllegalArgumentException(IllegalArgumentException ex) {
return "Error: " + ex.getMessage();
}
}
Desglosando paso a paso:
- El método
hellolanza una excepción si el parámetronamefalta. @ExceptionHandler(IllegalArgumentException.class)intercepta la excepciónIllegalArgumentException, y en lugar del stacktrace estándar se devuelve al usuario un mensaje comprensible.
Prueba a llamar a /hello sin el parámetro name. Verás: Error: ¡El nombre es obligatorio!.
Manejo de varios tipos de excepciones
Si tu controlador puede lanzar diferentes excepciones, puedes manejarlas por separado.
@Controller
public class MultiExceptionController {
@GetMapping("/data/{id}")
public String getData(@PathVariable("id") int id) {
if (id < 0) {
throw new IllegalArgumentException("El ID no puede ser negativo");
} else if (id == 0) {
throw new NullPointerException("El ID no puede ser cero");
}
return "Datos para ID " + id;
}
@ExceptionHandler(IllegalArgumentException.class)
@ResponseBody
public String handleIllegalArgument(IllegalArgumentException ex) {
return "Argumento inválido: " + ex.getMessage();
}
@ExceptionHandler(NullPointerException.class)
@ResponseBody
public String handleNullPointer(NullPointerException ex) {
return "Error: " + ex.getMessage();
}
}
Ahora podemos configurar respuestas diferentes para distintos tipos de errores:
- Si el usuario pide
/data/-1, verá:Argumento inválido: El ID no puede ser negativo. - Si pide
/data/0, verá:Error: El ID no puede ser cero.
Centralizar el manejo de errores: @ControllerAdvice
Si tienes más de dos controladores, lo correcto es sacar el manejo común de excepciones a una clase aparte. Para eso se usa la anotación @ControllerAdvice.
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
@ResponseBody
public String handleIllegalArgumentException(IllegalArgumentException ex) {
return "Error global: " + ex.getMessage();
}
@ExceptionHandler(Exception.class)
@ResponseBody
public String handleAllExceptions(Exception ex) {
return "Algo salió mal: " + ex.getMessage();
}
}
Ahora IllegalArgumentException y cualquier otra excepción lanzada en tu aplicación serán manejadas de forma centralizada. Puedes dejar manejo local en los controladores para casos específicos o eliminarlo por completo.
Cómo devolver respuestas bonitas: JSON en vez de texto
En aplicaciones reales solemos devolver respuestas en formato JSON, no mensajes de texto. Esto es especialmente importante para REST API.
Ejemplo con respuestas JSON
@ControllerAdvice
public class RestExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
@ResponseBody
public ResponseEntity<Map<String, String>> handleIllegalArgumentException(IllegalArgumentException ex) {
Map<String, String> response = new HashMap<>();
response.put("error", "Invalid argument");
response.put("message", ex.getMessage());
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}
}
Ahora, al solicitar /hello sin el parámetro name, obtendrás la siguiente respuesta JSON:
{
"error": "Invalid argument",
"message": "¡El nombre es obligatorio!"
}
Usando ResponseEntity también podemos indicar el status HTTP de la respuesta (por ejemplo, 400 - Bad Request).
Particularidades al trabajar con ValidationException
Si usas validación con la anotación @Valid, las excepciones lanzadas también deben manejarse. Por ejemplo:
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping
public String addUser(@Valid @RequestBody User user) {
return "User " + user.getName() + " añadido!";
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationException(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
}
public class User {
@NotNull(message = "El nombre no puede estar vacío")
private String name;
@Email(message = "Email no válido")
private String email;
// getters y setters
}
Si envías una petición sin nombre o con un email incorrecto, en la respuesta vendrá un JSON con la descripción de todos los errores.
Cómo probar correctamente el manejo de excepciones
Para testear los handlers puedes usar MockMvc. Así es como hacerlo:
@WebMvcTest(MultiExceptionController.class)
public class ExceptionHandlerTest {
@Autowired
private MockMvc mockMvc;
@Test
public void testHandleIllegalArgument() throws Exception {
mockMvc.perform(get("/data/-1"))
.andExpect(status().isBadRequest())
.andExpect(content().string("Argumento inválido: El ID no puede ser negativo"));
}
@Test
public void testHandleNullPointer() throws Exception {
mockMvc.perform(get("/data/0"))
.andExpect(status().isInternalServerError())
.andExpect(content().string("Error: El ID no puede ser cero"));
}
}
MockMvc te permite verificar que las excepciones se manejan correctamente y que se devuelven las respuestas esperadas.
Errores comunes al manejar excepciones
- Duplicar el manejo en
@ControllerAdvicey en los controladores. Si una excepción se maneja localmente, el handler global no se ejecutará. - No especificar el status HTTP. Si devuelves JSON, no te olvides de indicar el status HTTP correcto (por ejemplo, 400 para Bad Request).
- Interceptar la clase base
Exception. Si añades un handler paraException, taparás el manejo de excepciones específicas. Usa esto con cuidado.
Ahora sabes cómo manejar correctamente las excepciones en Spring MVC. La próxima vez, en vez del "horror rojo" del stacktrace, el usuario verá un mensaje claro y quedará satisfecho. ¡Buena suerte programando! 🚀
GO TO FULL VERSION