Tratamento de Erros
Tratar erros bem não significa apenas impedir que o programa “quebre”. Significa tornar falhas compreensíveis, localizar responsabilidades e preservar a clareza do fluxo normal da aplicação. Em código limpo, o tratamento de erros não pode competir com a regra de negócio pela atenção do leitor.
O que são exceções
Seção intitulada “O que são exceções”Exceções são mecanismos que representam situações anormais durante a execução de um programa. Em vez de espalhar verificações manuais a cada chamada, elas permitem separar o caminho principal do caminho de falha.
Em termos práticos, exceções ajudam a responder três perguntas:
- algo deu errado?
- onde deu errado?
- quem é responsável por reagir a isso?
Essa separação tende a produzir código mais legível do que soluções baseadas em códigos de retorno e convenções implícitas.
Exceções em vez de códigos de erro
Seção intitulada “Exceções em vez de códigos de erro”Retornar códigos como -1, null ou false para representar erro obriga cada chamador a lembrar da convenção adotada. Isso espalha lógica de tratamento por toda a cadeia de chamadas e enfraquece o significado do valor retornado.
Ruim:
int dividir(int a, int b, int &resultado) { if (b == 0) { return -1; } resultado = a / b; return 0;}Melhor:
int dividir(int a, int b) { if (b == 0) { throw std::runtime_error("Divisão por zero não permitida."); } return a / b;}Com exceções, o retorno volta a representar apenas sucesso, e o tratamento de falha pode ficar concentrado onde faz sentido.
Estruture o fluxo antes do tratamento
Seção intitulada “Estruture o fluxo antes do tratamento”Uma boa prática é pensar primeiro no fluxo normal do método e depois explicitar os pontos de exceção. Em muitos casos, montar a estrutura try-catch-finally desde o início ajuda a deixar claro:
- qual trecho pode falhar;
- qual política de recuperação será adotada;
- qual estado final precisa ser garantido.
Isso é especialmente útil em operações com IO, banco de dados, rede e integração externa.
Preserve o fluxo normal da aplicação
Seção intitulada “Preserve o fluxo normal da aplicação”O caminho principal do código deve continuar simples de ler. Casos excepcionais não podem dominar a narrativa.
Ruim:
public void processOrder(Order order) { if (order == null) { System.out.println("Error: Order is null"); return; } if (!order.isValid()) { System.out.println("Error: Invalid order"); return; } try { orderProcessor.process(order); } catch (Exception e) { System.out.println("Processing failed: " + e.getMessage()); }}Melhor:
public void processOrder(Order order) { validateOrder(order); try { orderProcessor.process(order); } catch (ProcessingException e) { throw new OrderProcessingException("Falha ao processar a ordem", e); }}No segundo caso, o fluxo principal fica evidente, e a validação é empurrada para uma abstração com nome significativo.
Use exceções não verificadas com critério
Seção intitulada “Use exceções não verificadas com critério”Em linguagens como Java, exceções verificadas obrigam assinatura e tratamento explícitos. Isso pode ser útil em alguns cenários, mas também pode poluir APIs quando a exceção precisa atravessar várias camadas até chegar ao lugar certo de tratamento.
Por isso, o ponto não é “nunca usar checked exceptions”, mas evitar que a infraestrutura de tratamento contamine todo o desenho da aplicação. Quando o custo de propagação é alto e a camada intermediária não tem como reagir, exceções não verificadas costumam simplificar a modelagem.
Forneça contexto suficiente
Seção intitulada “Forneça contexto suficiente”Uma exceção boa é informativa. Ela deve permitir que outra pessoa entenda o problema sem adivinhar o cenário.
Uma mensagem útil costuma responder:
- o que estava sendo feito;
- qual entidade ou recurso foi afetado;
- por que a operação falhou, quando isso é conhecido.
Exemplo ruim:
throw new RuntimeException("Erro");Exemplo melhor:
throw new OrderProcessingException( "Falha ao processar pedido 4832: gateway recusou a autorização do pagamento", e);Ao mesmo tempo, mensagens não devem vazar segredos, credenciais ou detalhes sensíveis de infraestrutura.
Não retorne null
Seção intitulada “Não retorne null”null como retorno empurra a responsabilidade para o chamador e frequentemente introduz código defensivo repetitivo. Em muitos projetos, isso vira a fonte mais comum de NullPointerException.
Alternativas melhores:
- lançar exceção quando a ausência representa erro;
- retornar coleção vazia em vez de
null; - usar
Optionalquando a ausência é esperada e semanticamente relevante; - aplicar Null Object Pattern quando um comportamento padrão fizer sentido.
Não passe null
Seção intitulada “Não passe null”Passar null como argumento também fragiliza contratos. Métodos passam a ter pré-condições ocultas: “funciona, exceto quando alguém esquecer tal coisa”.
Melhor abordagem:
- validar cedo;
- rejeitar dados inválidos com exceção clara;
- usar tipos que representem opcionalidade explicitamente.
Exemplo:
public void cadastrarCliente(Cliente cliente) { if (cliente == null) { throw new IllegalArgumentException("Cliente não pode ser nulo"); } // ...}Null Object Pattern
Seção intitulada “Null Object Pattern”O padrão Objeto Nulo substitui a ausência por uma implementação segura. Em vez de devolver null, devolve-se um objeto que respeita o mesmo contrato, mas com comportamento neutro.
Isso reduz verificações espalhadas e torna o fluxo mais estável. É particularmente útil quando o chamador pode continuar operando sem que a ausência represente erro fatal.
Ainda assim, use com critério: o padrão não deve mascarar problema de negócio real.
Optional
Seção intitulada “Optional”Optional é útil quando a ausência de valor faz parte do domínio. Ele torna essa possibilidade explícita no tipo, o que melhora a comunicação da API.
Exemplo de uso adequado:
- busca de usuário por email;
- leitura de configuração opcional;
- tentativa de localizar desconto aplicável.
Use Optional para comunicar incerteza, não para evitar pensar sobre contratos.
Separe lógica de negócios do tratamento de erro
Seção intitulada “Separe lógica de negócios do tratamento de erro”Métodos de negócio devem falar a linguagem do domínio. Logging, tradução de exceções técnicas e decisões de protocolo HTTP, por exemplo, pertencem a outras camadas.
Boa separação:
- serviço aplica regra de negócio e lança exceções de domínio;
- controlador/adaptador converte erro para resposta externa;
- infraestrutura registra logs e integra monitoramento.
Quando tudo fica no mesmo método, a lógica principal desaparece no meio de try, catch, printStackTrace, códigos de status e mensagens improvisadas.
Capture apenas o necessário
Seção intitulada “Capture apenas o necessário”catch (Exception e) é tentador, mas geralmente ruim. Ele captura erros demais, perde especificidade e dificulta reação adequada.
Melhor:
- capture exceções específicas;
- trate apenas as que você realmente consegue resolver ou enriquecer;
- deixe outras subirem para uma camada mais adequada.
Exemplo:
try (BufferedReader reader = new BufferedReader(new FileReader(caminho))) { reader.lines().forEach(System.out::println);} catch (FileNotFoundException e) { logger.error("Arquivo não encontrado: {}", caminho);} catch (IOException e) { logger.error("Erro de leitura no arquivo: {}", caminho, e);}Use finally para limpeza, ou mecanismos equivalentes
Seção intitulada “Use finally para limpeza, ou mecanismos equivalentes”Recursos abertos precisam ser liberados mesmo em caso de falha. Em Java moderno, prefira try-with-resources, mas o princípio continua o mesmo: conexão, stream, arquivo e cursor não devem depender de caminho feliz para serem fechados.
Falhas de limpeza podem causar:
- vazamento de conexão;
- arquivo bloqueado;
- degradação de desempenho;
- inconsistência de estado.
Evite falhas silenciosas
Seção intitulada “Evite falhas silenciosas”Capturar exceção e não fazer nada quase sempre é pior do que deixar a aplicação falhar. O sistema entra em estado indefinido, o usuário recebe comportamento estranho e a equipe perde capacidade de diagnóstico.
Ruim:
catch (SQLException e) { // não faz nada}Se o erro foi realmente absorvido por decisão consciente, essa escolha precisa ser explícita e justificada. Caso contrário, registre, traduza ou propague.
Logging e monitoramento
Seção intitulada “Logging e monitoramento”Logs devem complementar o tratamento, não substituí-lo. Um bom log ajuda a correlacionar contexto operacional, identificar frequência de falhas e apoiar observabilidade.
Boas práticas:
- use logs estruturados;
- inclua identificadores relevantes (pedido, usuário, operação);
- preserve a exceção original quando necessário;
- evite duplicar o mesmo erro em várias camadas sem necessidade.
Em sistemas reais, ferramentas como Sentry, Datadog, New Relic e Rollbar ampliam visibilidade de falhas em produção.
Não use exceções para controlar fluxo normal
Seção intitulada “Não use exceções para controlar fluxo normal”Exceções representam situações excepcionais. Usá-las como substituto de if/else ou switch piora desempenho e reduz clareza semântica.
Ruim:
if (numero < 12) { throw new TitularException();} else { throw new ReservaException();}Esse código não está tratando erro; está codificando decisão normal de negócio de forma distorcida.
Mensagens significativas e exceções personalizadas
Seção intitulada “Mensagens significativas e exceções personalizadas”Exceções específicas de domínio comunicam melhor do que classes genéricas como Exception ou RuntimeException.
Exemplos mais expressivos:
PedidoInvalidoExceptionPagamentoRecusadoExceptionSaldoInsuficienteExceptionUsuarioNaoAutorizadoException
Esses nomes ajudam em leitura, teste, logging e organização das políticas de tratamento.
Falhe alto e rápido
Seção intitulada “Falhe alto e rápido”Falhar rápido significa detectar inconsistências o mais cedo possível, perto da origem do problema. Isso reduz propagação do erro e facilita diagnóstico.
Exemplos práticos:
- validar entrada na borda do sistema;
- rejeitar estado impossível ao construir um objeto;
- não continuar processamento após detectar dado inválido;
- lançar erro cedo ao encontrar configuração obrigatória ausente.
Falhar alto significa também não esconder o problema: o erro precisa aparecer em um ponto observável.
Checklist prático
Seção intitulada “Checklist prático”Ao revisar tratamento de erros, pergunte:
- o fluxo normal do método continua legível?
- o erro está sendo tratado na camada certa?
- a exceção tem contexto suficiente?
- estou evitando
nullcomo contrato implícito? - o código está capturando algo específico ou genérico demais?
- há risco de falha silenciosa?
- logs e monitoramento ajudam no diagnóstico sem vazar informação sensível?