Jerarquía de excepciones en Java
Cómo se relacionan Throwable, Error, Exception y RuntimeException en la jerarquía de clases de excepciones de Java.
Cada excepción en Java forma parte de un pequeño árbol de clases cuya raíz es java.lang.Throwable. Conocer la forma de ese árbol resulta útil constantemente: explica por qué catch (Exception e) no captura OutOfMemoryError, por qué RuntimeException es especial y por qué algunas excepciones te obligan a manejarlas mientras que otras no. Todo el esquema cabe en un único diagrama.
Esta página traza ese árbol: la raíz Throwable, las ramas Error y Exception, dónde cae la línea entre verificadas y no verificadas, y cómo todo ello determina lo que tus bloques catch capturan realmente.
El árbol completo
Throwable
├── Error (unchecked — JVM-level)
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ ├── VirtualMachineError
│ └── ...
└── Exception (checked by default)
├── IOException (checked)
├── SQLException (checked)
├── ClassNotFoundException (checked)
├── ...
└── RuntimeException (unchecked)
├── NullPointerException
├── IllegalArgumentException
├── IndexOutOfBoundsException
├── ArithmeticException
├── ClassCastException
├── IllegalStateException
└── ...Todo el árbol es una única jerarquía de clases. Por eso un catch de un supertipo captura sus subtipos, por eso las variables de excepción se comportan como referencias ordinarias y por eso puedes almacenar una IOException en un campo de tipo Exception.
Throwable — la raíz
Throwable es lo que throw acepta y lo que catch declara. Cualquier cosa que quieras lanzar o manejar es una subclase de Throwable. La clase en sí proporciona:
- Un mensaje (
getMessage()) - Una traza de pila capturada en la construcción (
getStackTrace(),printStackTrace()) - Una causa opcional: otro
Throwableque desencadenó este (getCause()) - Excepciones suprimidas: fallos secundarios adjuntos por try-with-resources (
getSuppressed())
Casi nunca se extiende Throwable directamente. El diseño interesante vive un nivel por debajo.
Error — no lo captures
Error y sus subclases representan fallos que señala la JVM: memoria agotada, desbordamiento de pila, un archivo de clase que no puede enlazarse. Por convención no se capturan en el código de aplicación, porque:
- Generalmente indican que la JVM ya no es fiable. Continuar tras un
OutOfMemoryErrorrara vez funciona por mucho tiempo. - Casi nunca existe una acción de recuperación sensata que tu código pueda tomar.
- La propia JVM puede estar haciendo algo al respecto; interceptarlo interfiere.
Error es técnicamente capturable — Java no te lo impide. Pero la convención es tan fuerte que "captura Error" se considera una señal de alerta en la revisión de código. El único caso de uso legítimo es un supervisor de nivel superior (un manejador de peticiones, un ejecutor de tareas) que registra el error y termina limpiamente.
Exception — fallos de la aplicación
Todo lo que está bajo Throwable excepto Error es Exception o uno de sus subtipos. La línea entre verificadas y no verificadas corre dentro de esta rama, no por encima:
- Las subclases directas de
Exceptionque no sonRuntimeExceptionson verificadas. RuntimeExceptiony todos sus subtipos son no verificadas.
Por eso catch (Exception e) coincide tanto con IOException (verificada) como con NullPointerException (no verificada): son hermanas bajo la misma raíz. También es por eso que capturar Exception es tan genérico: has agrupado ambas ramas juntas.
La distinción entre verificadas y no verificadas tiene consecuencias reales para las firmas de tus métodos: las excepciones verificadas deben declararse con throws o manejarse, las no verificadas no. Ese intercambio se trata en detalle en excepciones verificadas vs. no verificadas.
RuntimeException — la rama de los errores de programación
RuntimeException y sus subtipos están reservados por convención para errores de programación que no deberían ocurrir en código correcto:
NullPointerException— desreferenciación de nullIllegalArgumentException— argumento incorrectoIllegalStateException— estado incorrecto para la operaciónIndexOutOfBoundsException— índice de lista/array fuera de los límitesArithmeticException— división por ceroClassCastException— conversión de tipo incorrectaUnsupportedOperationException— operación no soportada (p. ej., mutar una lista no modificable)
Puedes lanzarlas desde cualquier lugar sin cambiar la firma de tu método. Los llamadores pueden capturarlas, pero el lenguaje no los obliga. Son la herramienta adecuada cuando el fallo dice "esto es un error" en lugar de "esto ocurre a veces."
Relaciones de tipos en catch
Un catch (T e) coincide con cualquier valor lanzado que sea una instancia de T o un subtipo de T. Así que la jerarquía dicta directamente lo que ven tus catches:
try { ... }
catch (IOException e) { ... } // catches FileNotFoundException too
catch (Exception e) { ... } // catches almost everything below Throwable
catch (Throwable t) { ... } // catches everything, including Error — don'tEl orden importa: como cada catch coincide con los subtipos, debes listar los tipos más específicos primero. Si catch (Exception e) viniera antes que catch (IOException e), el bloque IOException sería inalcanzable y el compilador lo rechazaría. Consulta múltiples bloques catch para ver las reglas completas.
También es por eso que un patrón de captura universal es peligroso. catch (Exception) captura NullPointerException (un error de programación), IOException (un fallo recuperable) e IllegalStateException (probablemente un error), todo en un único bloque, sin forma de manejarlos de manera diferente. La jerarquía te pide que seas más específico.
Buscar tipos
Cuando encuentras una nueva excepción en una traza de pila y quieres saber dónde se ubica:
- Está en
java.langsi es un error fundamental (NullPointerException,ArithmeticException). - Está en
java.io,java.sql,java.netsi está relacionada con el dominio de ese paquete. - Una clase que termina en
Errorcasi seguramente está bajoError. - Una clase que termina en
Exceptioncasi seguramente está bajoException, pero comprueba si extiendeRuntimeExceptionpara saber si es verificada.
El Javadoc muestra la cadena de herencia en la parte superior de cada página. En caso de duda, consúltalo.
Un ejemplo práctico
Un pequeño programa que recorre la jerarquía con comprobaciones instanceof. Captura una secuencia de lanzamientos como Throwable y luego informa de dónde se ubica cada uno en el árbol.
El helper isChecked codifica la regla en una sola línea: el subconjunto verificado es Exception menos RuntimeException. Ejecuta el programa y verás exactamente cuál de los cinco se ubica dónde: IOException es verificada, los dos RuntimeException no lo son, el OutOfMemoryError es un Error (por lo que no es ni Exception ni verificado) y la Exception simple es verificada.
Qué sigue
El árbol incorporado cubre la mayoría de los casos. Cuando tu dominio tiene sus propios fallos —"factura no encontrada", "configuración desactualizada"— escribes tus propias clases extendiendo el nodo adecuado de este árbol. Continúa en excepciones personalizadas en Java.
Lecturas relacionadas:
- Conceptos básicos de try-catch — cómo
catchselecciona realmente un manejador. throwythrows— lanzar excepciones y declarar las verificadas.- Verificadas vs. no verificadas — la línea dentro de la rama
Exception. - Buenas prácticas con excepciones — qué hacer con lo que capturas.