CodeGym /Cursos /Módulo 5. Spring /Manejo de excepciones en controladores usando @ExceptionH...

Manejo de excepciones en controladores usando @ExceptionHandler

Módulo 5. Spring
Nivel 7 , Lección 9
Disponible

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:

  1. Mostrar al usuario mensajes comprensibles en vez de "algo salió mal".
  2. Centralizar el control de errores.
  3. Minimizar el caos en el código de los controladores.
  4. 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:

  1. El método hello lanza una excepción si el parámetro name falta.
  2. @ExceptionHandler(IllegalArgumentException.class) intercepta la excepción IllegalArgumentException, 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

  1. Duplicar el manejo en @ControllerAdvice y en los controladores. Si una excepción se maneja localmente, el handler global no se ejecutará.
  2. No especificar el status HTTP. Si devuelves JSON, no te olvides de indicar el status HTTP correcto (por ejemplo, 400 para Bad Request).
  3. Interceptar la clase base Exception. Si añades un handler para Exception, 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! 🚀

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION