Pular para o conteúdo

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.

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.

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.

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.

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.

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.

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.

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 Optional quando a ausência é esperada e semanticamente relevante;
  • aplicar Null Object Pattern quando um comportamento padrão fizer sentido.

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");
}
// ...
}

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 é ú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.

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.

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.

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.

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.

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:

  • PedidoInvalidoException
  • PagamentoRecusadoException
  • SaldoInsuficienteException
  • UsuarioNaoAutorizadoException

Esses nomes ajudam em leitura, teste, logging e organização das políticas de tratamento.

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.

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 null como 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?