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
Throwableque 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:
- Geralmente significam que a JVM não é mais confiável. Continuar após um
OutOfMemoryErrorraramente funciona por muito tempo. - Quase nunca há uma ação de recuperação sensata que seu código possa realizar.
- 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
Exceptionque não sãoRuntimeExceptionsão checked. RuntimeExceptione 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 nullIllegalArgumentException— argumento inválidoIllegalStateException— estado errado para a operaçãoIndexOutOfBoundsException— índice de lista/array além do limiteArithmeticException— divisão por zeroClassCastException— cast inválidoUnsupportedOperationException— 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'tA 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.langse for um erro fundamental (NullPointerException,ArithmeticException). - Ela está em
java.io,java.sql,java.netse estiver relacionada ao domínio daquele pacote. - Uma classe que termina em
Errorquase certamente está sobError. - Uma classe que termina em
Exceptionquase certamente está sobException— mas verifique se ela estendeRuntimeExceptionpara 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.
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:
- Fundamentos de try-catch — como
catchseleciona um handler. throwethrows— lançando exceções e declarando as checked.- Checked vs. unchecked — a linha dentro do ramo
Exception. - Boas práticas com exceções — o que fazer com o que você captura.