W3docs

Hierarquia de Exceções em Java

Como Throwable, Error, Exception e RuntimeException se relacionam na hierarquia de classes de exceção do Java.

Toda exceção em Java faz parte de uma pequena árvore de classes com raiz em java.lang.Throwable. Conhecer a forma dessa árvore é sempre útil: ela explica por que catch (Exception e) não captura OutOfMemoryError, por que RuntimeException é especial e por que algumas exceções obrigam você a tratá-las enquanto outras não. O layout completo cabe em um único diagrama.

Esta página mapeia essa árvore: a raiz Throwable, os ramos Error e Exception, onde está a linha entre checked e unchecked, e como tudo isso determina o que seus blocos catch realmente capturam.

A árvore completa

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
        └── ...

A árvore inteira é uma hierarquia de classes. É por isso que um catch para um supertipo captura seus subtipos, por que variáveis de exceção se comportam como referências comuns e por que você pode armazenar um IOException em um campo do tipo Exception.

Throwable — a raiz

Throwable é o que throw aceita e o que catch declara. Qualquer coisa que você queira lançar ou tratar é uma subclasse de Throwable. A própria classe fornece:

  • Uma mensagem (getMessage())
  • Um stack trace capturado na construção (getStackTrace(), printStackTrace())
  • Uma causa opcional — outro Throwable que originou este (getCause())
  • Exceções suprimidas — falhas secundárias anexadas pelo try-with-resources (getSuppressed())

Você quase nunca estende Throwable diretamente. O design interessante está um nível abaixo.

Error — não capture

Error e suas subclasses representam falhas sinalizadas pela JVM: falta de memória, estouro de pilha, um arquivo de classe que não pode ser vinculado. Por convenção, você não as captura no código da aplicação, porque:

  1. Geralmente significam que a JVM não é mais confiável. Continuar após um OutOfMemoryError raramente funciona por muito tempo.
  2. Quase nunca há uma ação de recuperação sensata que seu código possa realizar.
  3. A própria JVM pode já estar fazendo algo a respeito; interceptar interfere.

Error é tecnicamente capturável — o Java não impede você. Mas a convenção é tão forte que "capturar Error" é tratado como um sinal de alerta em revisões de código. O único caso de uso legítimo é um supervisor de nível superior (um handler de requisição, um executor de tarefas) que registra o erro e encerra de forma limpa.

Exception — falhas da aplicação

Tudo que não é Error sob Throwable é Exception ou um de seus subtipos. A linha entre checked e unchecked está dentro deste ramo, não acima dele:

  • Subclasses diretas de Exception que não são RuntimeException são checked.
  • RuntimeException e todos os seus subtipos são unchecked.

É por isso que catch (Exception e) corresponde tanto a IOException (checked) quanto a NullPointerException (unchecked) — elas são irmãs sob a mesma raiz. É também por isso que capturar Exception é tão amplo: você agrupou os dois ramos juntos.

A distinção checked/unchecked tem consequências reais para as assinaturas dos seus métodos — exceções checked devem ser declaradas com throws ou tratadas; as unchecked não precisam. Essa troca é tratada em detalhes em checked vs. unchecked exceptions.

RuntimeException — o ramo de bugs

RuntimeException e seus subtipos são reservados por convenção para erros de programação que não deveriam ocorrer em código correto:

  • NullPointerException — desreferenciando null
  • IllegalArgumentException — argumento inválido
  • IllegalStateException — estado errado para a operação
  • IndexOutOfBoundsException — índice de lista/array além do limite
  • ArithmeticException — divisão por zero
  • ClassCastException — cast inválido
  • UnsupportedOperationException — operação não suportada (ex.: mutando uma lista imutável)

Você pode lançar essas exceções de qualquer lugar sem alterar a assinatura do método. Os chamadores podem capturá-las, mas a linguagem não os obriga. Elas são a ferramenta certa quando a falha significa "isso é um bug" em vez de "isso acontece às vezes."

Relações de tipos no catch

Um catch (T e) corresponde a qualquer valor lançado que seja uma instância de T ou de um subtipo de T. Portanto, a hierarquia determina diretamente o que seus catches veem:

try { ... }
catch (IOException e)        { ... }   // catches FileNotFoundException too
catch (Exception e)          { ... }   // catches almost everything below Throwable
catch (Throwable t)          { ... }   // catches everything, including Error — don't

A ordem importa aqui: como cada catch corresponde a subtipos, você deve listar os tipos mais específicos primeiro. Se catch (Exception e) viesse antes de catch (IOException e), o bloco IOException seria inacessível e o compilador o rejeitaria. Veja múltiplos blocos catch para as regras completas.

É também por isso que capturar tudo de uma vez é perigoso. catch (Exception) captura NullPointerException (um bug), IOException (uma falha recuperável) e IllegalStateException (provavelmente um bug) — tudo em um único bloco, sem como tratá-los de forma diferente. A hierarquia pede que você seja mais específico.

Consultando tipos

Quando você encontrar uma nova exceção em um stack trace e quiser saber onde ela se encontra:

  • Ela está em java.lang se for um erro fundamental (NullPointerException, ArithmeticException).
  • Ela está em java.io, java.sql, java.net se estiver relacionada ao domínio daquele pacote.
  • Uma classe que termina em Error quase certamente está sob Error.
  • Uma classe que termina em Exception quase certamente está sob Exception — mas verifique se ela estende RuntimeException para saber se é checked.

O Javadoc mostra a cadeia de herança no topo de cada página. Em caso de dúvida, consulte-o.

Um exemplo prático

Um pequeno programa que percorre a hierarquia com verificações instanceof. Ele captura uma sequência de lançamentos como Throwable e depois informa onde cada um se encontra na árvore.

java— editable, runs on the server

O helper isChecked codifica a regra em uma linha: o subconjunto checked é Exception menos RuntimeException. Execute o programa e você verá exatamente qual dos cinco se encontra onde: IOException é checked, as duas RuntimeExceptions não são, o OutOfMemoryError é um Error (portanto não é uma Exception nem checked), e a Exception simples é checked.

O que vem a seguir

A árvore embutida cobre a maioria dos casos. Quando seu domínio tem suas próprias falhas — "fatura não encontrada", "configuração desatualizada" — você escreve suas próprias classes estendendo o nó correto nessa árvore. Continue em Java custom exceptions.

Leituras relacionadas:

Prática

Prática
Um programa usa `catch (Exception e)` em torno de um bloco. Quais destes serão capturados?
Um programa usa `catch (Exception e)` em torno de um bloco. Quais destes serão capturados?
Was this page helpful?