W3docs

Buenas prácticas para el manejo de excepciones en Java

Reglas prácticas para el manejo de excepciones en Java: falla rápido, lanza el tipo correcto, nunca ignores excepciones y registra información útil.

Los capítulos anteriores cubrieron la mecánica — try, catch, finally, throw, throws, clases personalizadas. Este capítulo trata el lado del criterio. Dos programas pueden usar los mismos constructos y uno es robusto mientras el otro es frágil. La diferencia es un pequeño conjunto de hábitos que se convierten en reflejo con la práctica.

Falla rápido ante entradas incorrectas

Si un método es llamado con argumentos que no puede manejar, lanza una excepción de inmediato:

public void send(String to, String body) {
  if (to == null || to.isBlank()) {
    throw new IllegalArgumentException("to must be non-blank");
  }
  if (body == null) {
    throw new NullPointerException("body");
  }
  // ...real work
}

No intentes "hacer lo mejor posible" con null. El error está en quien llama al método, y cuanto más cerca aterrice la excepción de ese punto, más fácil es corregirlo. Objects.requireNonNull(body, "body") es la solución estándar de una línea para el caso de null.

El hábito contrario — sustituir silenciosamente valores predeterminados por entradas faltantes — produce errores que aparecen cinco capas más adelante sin ninguna pista sobre quién pasó qué.

Lanza el tipo más específico que corresponda

Exception rara vez es lo correcto para lanzar, y RuntimeException solo cuando no existe un tipo más específico. La biblioteca estándar te proporciona un vocabulario — úsalo:

  • Argumento incorrecto → IllegalArgumentException
  • Argumento null → NullPointerException (Objects.requireNonNull)
  • Estado incorrecto → IllegalStateException
  • Operación no soportada → UnsupportedOperationException
  • Índice fuera de rango → IndexOutOfBoundsException
  • Número fuera de rango → ArithmeticException o IllegalArgumentException

Para fallos de dominio, escribe una excepción personalizada en lugar de reutilizar una integrada.

Captura el tipo más específico para el que tienes un plan

La regla simétrica. catch (Exception e) es una señal de alerta. Captura errores de programación (NullPointerException, IllegalStateException) y fallos recuperables (IOException) y excepciones desconocidas de bibliotecas en un solo bloque — y casi siempre las maneja de forma idéntica, lo cual casi siempre es incorrecto.

// Bad — what does this even handle?
try { complex(); }
catch (Exception e) { log("failed"); }

// Better — specific cases get specific responses
try { complex(); }
catch (IOException e)         { retryLater(); }
catch (ParseException e)      { recordCorruptInput(e); }

Cuando genuinamente no sabes qué hacer con una clase de excepción, la respuesta es no capturarla. Deja que se propague a un manejador que sí sepa.

Nunca ignores una excepción silenciosamente

El peor patrón único de manejo de excepciones:

try { doWork(); }
catch (Exception e) { }    // never write this

Cuando inevitablemente aparezca el error en producción, no habrá rastro de pila, ni mensaje, ni entrada en el registro — el fallo simplemente desapareció. Si genuinamente pretendes ignorar un fallo (raro, pero posible — por ejemplo, cerrar un recurso en una ruta de limpieza), dilo explícitamente:

try { connection.close(); }
catch (IOException ignored) {
  // close-time failure on a cleanup path; original cause already propagating
}

El nombre de variable ignored y el comentario hacen visible la intención al siguiente lector.

Registra de forma útil, registra una sola vez

Dos fallos comunes en el registro:

  • Registrar sin suficiente contextolog.error("failed") no te dice nada.
  • Registrar y volver a lanzar — cada capa registra la misma excepción y el mismo rastro termina en el registro cinco veces.

Elige una capa que conozca el mayor contexto (generalmente de alto nivel: manejador de solicitudes, ejecutor de tareas) y registra allí con la entrada que desencadenó el fallo. Las capas inferiores deben centrarse en traducir la excepción, no en registrarla.

try {
  userService.activate(id);
} catch (UserNotFoundException e) {
  log.warn("activation failed: no user with id={}", id, e);   // include the exception object as the last arg
  return Response.notFound();
}

Pasar la excepción como último argumento del logger es la convención de SLF4J — garantiza que el rastro de pila completo y la cadena de causas aparezcan en la salida.

Preserva la causa al envolver excepciones

Cuando traduces una excepción a una capa superior, siempre pasa la original como causa:

// Good — cause is preserved
catch (IOException e) {
  throw new ConfigLoadException("failed to load " + path, e);
}

// Bad — original IOException is lost
catch (IOException e) {
  throw new ConfigLoadException("failed to load " + path);
}

La cadena Caused by: en el rastro de pila resultante es lo que le permite al ingeniero de guardia rastrear una excepción de dominio hasta el fallo a nivel de bytes. Perderla convierte una sesión de depuración de media hora en medio día.

No uses excepciones para el flujo de control

Lanzar es costoso — construir un rastro de pila en la construcción es el mayor costo. Más importante aún, enturbia la intención. Un bucle que usa try/catch (NoSuchElementException) para saber cuándo detenerse está ocultando lo que hace:

// Bad
try {
  while (true) {
    process(iter.next());
  }
} catch (NoSuchElementException end) { }

// Good
while (iter.hasNext()) {
  process(iter.next());
}

Cuando "no encontrado" es un resultado ordinario, devuelve Optional<T> o un boolean. Reserva las excepciones para lo verdaderamente excepcional.

Usa finally y try-with-resources para la limpieza

finally debe liberar recursos. try-with-resources debe ser el predeterminado para cualquier cosa AutoCloseable. No pongas lógica de negocio en finally — se ejecuta tanto en la ruta de éxito como en la de fallo y no puede distinguirlas. Y no uses return en finally — descarta silenciosamente la excepción original o el valor de retorno, lo que es uno de los errores más difíciles de diagnosticar.

Documenta lo que lanzas

Si un método puede lanzar una excepción que importa a quienes lo llaman — comprobada o no — dilo en el Javadoc:

/**
 * Looks up a user by id.
 *
 * @throws UserNotFoundException if no user with that id exists
 * @throws IllegalArgumentException if id is null or blank
 */
public User lookup(String id) { ... }

El compilador lo aplica para las excepciones comprobadas en la firma. Para las no comprobadas, el Javadoc es el único contrato — y quienes llaman al método realmente lo necesitan cuando la excepción afecta cómo deben usarlo.

Usa excepciones para fallos, valores de retorno para resultados rutinarios

La regla resumen, y la que une el resto. Una excepción dice algo salió mal que no puedo arreglar aquí. Un valor de retorno dice aquí está el resultado. Cuando "no encontrado" es parte de la operación normal, devuelve Optional.empty(), un boolean o un centinela. Cuando "la conexión a la base de datos se perdió" ocurre, lanza.

El código que respeta esta distinción es tranquilo: la ruta feliz parece una línea recta, la ruta inusual está en un bloque diferente, y el lector puede ver de un vistazo cuál es cuál.

Un ejemplo completo

Una pequeña función de procesamiento de pedidos que reúne las prácticas de este capítulo — validación que falla rápido, tipos de excepción integrados específicos, envoltura con causa y un único manejador de nivel superior que registra una sola vez.

java— editable, runs on the server

Cuatro llamadas, cuatro rutas diferentes. La exitosa retorna normalmente. Los dos casos de IllegalArgumentException (id null, cantidad cero) se reportan con el mensaje que explica qué estaba mal con la entrada. El fallo simulado del servicio aparece como una OrderProcessingException de dominio con la IllegalStateException original enlazada a través de getCause(). Nada se ignora, nada se registra dos veces, y cada fallo dice exactamente qué valor lo causó.

¿Qué sigue?

Eso cierra la Parte 8 — tienes un dominio funcional del mecanismo de excepciones de Java y el criterio para usarlo bien. La siguiente parte hace un recorrido profundo por los strings — el tipo más común en el código Java con diferencia y uno con más profundidad de la que su superficie sugiere. Continúa en Clase String de Java.

Práctica

Práctica
¿Cuál de estos hábitos de manejo de excepciones es **más** probable que haga que la depuración en producción sea dolorosa?
¿Cuál de estos hábitos de manejo de excepciones es **más** probable que haga que la depuración en producción sea dolorosa?
Was this page helpful?