Boas Práticas de Tratamento de Exceções em Java
Regras práticas para tratamento de exceções em Java: falhe rápido, lance o tipo certo, nunca engula exceções e registre com utilidade.
Os capítulos anteriores cobriram a mecânica — try, catch, finally, throw, throws, classes personalizadas. Este é o lado do julgamento. Dois programas podem usar os mesmos construtos e um ser robusto enquanto o outro é frágil. A diferença está em um pequeno conjunto de hábitos que se tornam reflexo com a prática.
Falhe rápido em entradas inválidas
Se um método for chamado com argumentos que ele não consegue processar, lance imediatamente:
public void send(String to, String body) {
if (to == null || to.isBlank()) {
throw new IllegalArgumentException("to must be non-blank");
}
if (body == null) {
throw new NullPointerException("body");
}
// ...real work
}Não tente "fazer o melhor possível" com null. O bug está no chamador, e quanto mais próxima a exceção cair dele, mais fácil será corrigi-lo. Objects.requireNonNull(body, "body") é o one-liner padrão para o caso de null.
O hábito oposto — substituir silenciosamente entradas ausentes por valores padrão — leva a erros que surgem cinco camadas adiante sem nenhuma pista sobre quem passou o quê.
Lance o tipo mais específico que se encaixar
Exception raramente é a coisa certa a lançar, e RuntimeException apenas quando nenhum tipo mais específico se encaixa. A biblioteca padrão fornece um vocabulário — use-o:
- Argumento inválido →
IllegalArgumentException - Argumento null →
NullPointerException(Objects.requireNonNull) - Estado incorreto →
IllegalStateException - Operação não suportada →
UnsupportedOperationException - Índice fora do intervalo →
IndexOutOfBoundsException - Número fora do intervalo →
ArithmeticExceptionouIllegalArgumentException
Para falhas de domínio, escreva uma exceção personalizada em vez de reutilizar uma built-in.
Capture o tipo mais específico para o qual você tem um plano
A regra simétrica. catch (Exception e) é um sinal de alerta. Ela captura bugs de programação (NullPointerException, IllegalStateException) e falhas recuperáveis (IOException) e exceções de biblioteca desconhecidas tudo em um único balde — e quase sempre as trata de forma idêntica, o que quase sempre está errado.
// Bad — what does this even handle?
try { complex(); }
catch (Exception e) { log("failed"); }
// Better — specific cases get specific responses
try { complex(); }
catch (IOException e) { retryLater(); }
catch (ParseException e) { recordCorruptInput(e); }Quando você genuinamente não sabe o que fazer com uma classe de exceção, a resposta é não a capture. Deixe-a propagar para um handler que saiba.
Nunca engula uma exceção silenciosamente
O pior padrão único de tratamento de exceções:
try { doWork(); }
catch (Exception e) { } // never write thisQuando o inevitável bug de produção ocorrer, não há stack trace, nenhuma mensagem, nenhuma entrada de log — a falha simplesmente desapareceu. Se você genuinamente pretende ignorar uma falha (raro, mas possível — por exemplo, fechar um recurso em um caminho de limpeza), diga isso explicitamente:
try { connection.close(); }
catch (IOException ignored) {
// close-time failure on a cleanup path; original cause already propagating
}O nome de variável ignored e o comentário tornam a intenção visível para o próximo leitor.
Registre de forma útil, registre uma vez
Dois problemas de logging são comuns:
- Registrar sem contexto suficiente —
log.error("failed")não diz nada. - Registrar e depois relançar — cada camada registra a mesma exceção, e o mesmo trace acaba no log cinco vezes.
Escolha uma camada que conheça o máximo de contexto (geralmente de alto nível: request handler, job runner) e registre lá com o input que desencadeou a falha. As camadas abaixo devem se concentrar em traduzir a exceção, não em registrá-la.
try {
userService.activate(id);
} catch (UserNotFoundException e) {
log.warn("activation failed: no user with id={}", id, e); // include the exception object as the last arg
return Response.notFound();
}Passar a exceção como o último argumento do logger é a convenção SLF4J — ela garante que o stack trace completo e qualquer cadeia de causa apareçam na saída.
Preserve a causa ao encapsular
Ao traduzir uma exceção para uma camada superior, sempre passe o original como a causa:
// Good — cause is preserved
catch (IOException e) {
throw new ConfigLoadException("failed to load " + path, e);
}
// Bad — original IOException is lost
catch (IOException e) {
throw new ConfigLoadException("failed to load " + path);
}A cadeia Caused by: no stack trace resultante é o que permite ao engenheiro de plantão rastrear uma exceção de domínio de volta à falha no nível de bytes. Perca-a e uma sessão de depuração de meia hora se torna um meio dia.
Não use exceções para controle de fluxo
Lançar é caro — construir um stack trace na criação é o maior custo. Mais importante, isso obscurece a intenção. Um loop que usa try/catch (NoSuchElementException) para saber quando parar está escondendo o que está fazendo:
// Bad
try {
while (true) {
process(iter.next());
}
} catch (NoSuchElementException end) { }
// Good
while (iter.hasNext()) {
process(iter.next());
}Quando "não encontrado" é um resultado comum, retorne Optional<T> ou um boolean. Reserve exceções para o verdadeiramente excepcional.
Use finally e try-with-resources para limpeza
finally deve liberar recursos. try-with-resources deve ser o padrão para qualquer coisa AutoCloseable. Não coloque lógica de negócio em finally — ela é executada em ambos os caminhos de sucesso e falha e não pode diferenciá-los. E não use return em finally — ele descarta silenciosamente a exceção original ou o valor de retorno, o que é um dos bugs mais difíceis de diagnosticar.
Documente o que você lança
Se um método pode lançar uma exceção que importa aos chamadores — verificada ou não verificada — diga no Javadoc:
/**
* Looks up a user by id.
*
* @throws UserNotFoundException if no user with that id exists
* @throws IllegalArgumentException if id is null or blank
*/
public User lookup(String id) { ... }O compilador aplica isso para exceções verificadas na assinatura. Para as não verificadas, o Javadoc é o único contrato — e os chamadores realmente precisam dele quando a exceção afeta como devem usar o método.
Use exceções para falhas, retornos para resultados rotineiros
A regra resumida, e a que une todas as demais. Uma exceção diz algo deu errado que não consigo corrigir aqui. Um valor de retorno diz aqui está o resultado. Quando "não encontrado" faz parte da operação normal, retorne Optional.empty(), um boolean ou um sentinel. Quando "a conexão com o banco de dados caiu" acontecer, lance.
Código que observa essa distinção é tranquilo: o caminho feliz parece uma linha reta, o caminho incomum está em um bloco diferente, e o leitor consegue dizer de relance qual é qual.
Um exemplo trabalhado
Uma pequena função de processamento de pedidos que reúne as práticas deste capítulo — validação fail-fast, tipos de exceção built-in específicos, encapsulamento com causa e um único handler de nível superior que registra uma vez.
Quatro chamadas, quatro caminhos diferentes. A bem-sucedida retorna normalmente. Os dois casos de IllegalArgumentException (id null, amount zero) são reportados com a mensagem que explica o que estava errado com o input. A falha de serviço simulada aparece como um OrderProcessingException de domínio com o IllegalStateException original vinculado por meio de getCause(). Nada é engolido, nada é registrado duas vezes e cada falha indica exatamente qual valor a causou.
O que vem a seguir
Isso encerra a Parte 8 — você tem domínio funcional da maquinaria de exceções do Java e o julgamento para usá-la bem. A próxima parte faz um tour aprofundado pelas strings — o tipo mais comum no código Java por uma grande margem e um com mais profundidade do que sua superfície sugere. Continue para Java String class.