1. Introducción
En esta lección analizaremos una técnica importante para trabajar con excepciones — el encadenamiento de excepciones (exception chaining). Esta técnica permite no perder información sobre la causa original del error, incluso cuando «envuelves» una excepción dentro de otra.
En aplicaciones reales, a menudo sucede que un error aparece en lo profundo de la pila de llamadas — por ejemplo, al trabajar con una base de datos, el sistema de archivos o la red. Supongamos que tienes un método que accede a la base de datos y puede lanzar SQLException. Pero a nivel de lógica de negocio no quieres «ensuciar» el código con detalles técnicos y prefieres lanzar tu propia excepción, por ejemplo, UserManagementException.
¿Qué pasa si simplemente lanzas una nueva excepción?
try {
// algo con la base de datos
} catch (SQLException e) {
throw new UserManagementException("Error al trabajar con usuarios");
}
Problema:
En ese caso, la información sobre lo que exactamente ocurrió en la base de datos (¡y la pila de llamadas!) se pierde. En el log solo verás UserManagementException, y cuál fue la causa — desconocido.
2. Solución: envolver la excepción original (chaining)
Java permite «envolver» una excepción dentro de otra, pasando la excepción original como causa (cause) al constructor de la nueva excepción. A esto se le llama encadenamiento de excepciones.
¿Cómo hacerlo?
La mayoría de las excepciones estándar y personalizadas tienen un constructor que acepta un segundo parámetro — Throwable cause:
public UserManagementException(String message, Throwable cause) {
super(message, cause);
}
Uso:
try {
// algo con la base de datos
} catch (SQLException e) {
throw new UserManagementException("Error al trabajar con usuarios", e);
}
Ahora, si miras la pila de llamadas (printStackTrace()), verás tu excepción y toda la cadena hasta la causa original.
3. Cómo obtener la causa de una excepción
Cualquier objeto de tipo Throwable tiene el método getCause(), que devuelve la excepción original (o null si no existe).
Ejemplo:
try {
// ...
} catch (UserManagementException e) {
Throwable cause = e.getCause();
if (cause != null) {
System.out.println("Causa raíz: " + cause);
}
e.printStackTrace();
}
¿Para qué sirve?
- Para depuración: ves no solo «qué salió mal» en el nivel superior, sino también dónde ocurrió exactamente el error en lo profundo de la pila.
- Para el registro en logs: puedes escribir en el log toda la cadena de errores.
- Para transferir información entre capas de la aplicación: la capa de negocio puede «envolver» la excepción técnica en la suya sin perder detalles.
4. Ejemplo: encadenamiento de excepciones en una aplicación real
Supongamos que tienes un método que carga un usuario desde la base de datos:
public User loadUser(String username) throws UserManagementException {
try {
// Código que puede lanzar SQLException
// ...
} catch (SQLException e) {
throw new UserManagementException("No se pudo cargar al usuario: " + username, e);
}
}
Donde UserManagementException es tu propia excepción:
public class UserManagementException extends Exception {
public UserManagementException(String message, Throwable cause) {
super(message, cause);
}
}
¿Qué ocurrirá en caso de error?
- En los logs se verán tanto tu excepción como el SQLException original con todos los detalles.
- Si es necesario, puedes acceder a la causa original mediante getCause().
5. Cómo se ve la pila de llamadas con encadenamiento de excepciones
Ejemplo de salida:
UserManagementException: No se pudo cargar al usuario: example_user
at UserService.loadUser(UserService.java:15)
...
Caused by: java.sql.SQLException: Connection refused
at ...
Aquí se ve todo: la cadena completa de llamadas, dónde surgió el error de negocio y qué excepción técnica fue la causa.
6. Práctica: implementemos el encadenamiento de excepciones
Paso 1. Creamos nuestra propia excepción:
public class UserManagementException extends Exception {
public UserManagementException(String message) {
super(message);
}
public UserManagementException(String message, Throwable cause) {
super(message, cause);
}
}
Paso 2. Usamos el encadenamiento:
try {
// algo peligroso
} catch (SQLException e) {
throw new UserManagementException("Error al trabajar con la BD", e);
}
Paso 3. Manejo en el nivel superior:
En el nivel más alto del programa capturamos nuestra propia excepción y mostramos el mensaje de error junto con toda la cadena de causas.
public class Main {
public static void main(String[] args) {
try {
runUserManagement();
} catch (UserManagementException e) {
System.err.println("Se produjo un error: " + e.getMessage());
// Imprimimos la cadena de causas
Throwable cause = e.getCause();
while (cause != null) {
System.err.println("Causa: " + cause.getMessage());
cause = cause.getCause();
}
}
}
private static void runUserManagement() throws UserManagementException {
try {
// simulación de error de BD
throw new SQLException("No hay conexión con la BD");
} catch (SQLException e) {
throw new UserManagementException("Error al trabajar con la BD", e);
}
}
}
7. Errores típicos al trabajar con cadenas de excepciones
Error n.º 1: lanzas una nueva excepción sin la causa.
catch (SQLException e) {
throw new UserManagementException("Error", /* ¡sin cause! */);
}
Malo: se pierde la información sobre la causa original.
Error n.º 2: no implementaste un constructor con cause en tu propia excepción.
Si tu clase de excepción no tiene un constructor con Throwable cause, no podrás pasar la causa — tendrás que añadirlo manualmente.
Error n.º 3: capturas y silencias la excepción sin propagarla.
catch (SQLException e) {
// Solo registramos y callamos
}
Malo: el error «se pierde» y el programa sigue funcionando de forma incorrecta.
try {
userService.loadUser("example_user");
} catch (UserManagementException e) {
System.err.println("Error: " + e.getMessage());
if (e.getCause() != null) {
System.err.println("Causa raíz: " + e.getCause());
}
e.printStackTrace();
}
GO TO FULL VERSION