W3docs

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 Throwable que 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:

  1. Generalmente indican que la JVM ya no es fiable. Continuar tras un OutOfMemoryError rara vez funciona por mucho tiempo.
  2. Casi nunca existe una acción de recuperación sensata que tu código pueda tomar.
  3. 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 Exception que no son RuntimeException son verificadas.
  • RuntimeException y 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 null
  • IllegalArgumentException — argumento incorrecto
  • IllegalStateException — estado incorrecto para la operación
  • IndexOutOfBoundsException — índice de lista/array fuera de los límites
  • ArithmeticException — división por cero
  • ClassCastException — conversión de tipo incorrecta
  • UnsupportedOperationException — 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't

El 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.lang si es un error fundamental (NullPointerException, ArithmeticException).
  • Está en java.io, java.sql, java.net si está relacionada con el dominio de ese paquete.
  • Una clase que termina en Error casi seguramente está bajo Error.
  • Una clase que termina en Exception casi seguramente está bajo Exception, pero comprueba si extiende RuntimeException para 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.

java— editable, runs on the server

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:

Práctica

Práctica
Un programa hace `catch (Exception e)` alrededor de un bloque. ¿Cuál de estos será capturado?
Un programa hace `catch (Exception e)` alrededor de un bloque. ¿Cuál de estos será capturado?
Was this page helpful?