Tratamento de erros: falhar bem sem engolir exceções
Erro faz parte do fluxo normal do software. Entenda por que engolir exceções é um dos piores hábitos e como falhar de forma clara, com contexto e limpeza.
Neste artigo
Existe uma tentação quase irresistível, especialmente no início da carreira, de tratar erros como interrupções indesejadas do trabalho de verdade. O programador escreve o "caminho feliz", aquele em que tudo dá certo, e só depois, meio a contragosto, adiciona algum tratamento de erro para calar o compilador ou fazer o teste passar. Nessa visão, o erro é um estorvo, algo que atrapalha a lógica principal e que se resolve da forma mais rápida possível para poder voltar ao que importa.
Essa postura é a raiz de uma quantidade impressionante de bugs difíceis de diagnosticar. Sistemas que falham silenciosamente, dados que se corrompem sem que ninguém perceba, comportamentos misteriosos que ninguém consegue reproduzir, boa parte disso nasce de erros que foram tratados de forma displicente, escondidos debaixo do tapete em vez de enfrentados de frente. O erro engolido não desaparece; ele apenas adia o momento em que suas consequências vão aparecer, geralmente em um lugar distante e num formato irreconhecível.
Este artigo defende uma mudança de mentalidade: erros não são exceções ao fluxo, são parte dele. Programar bem inclui projetar como o sistema se comporta quando as coisas dão errado, com o mesmo cuidado dedicado ao caso em que dão certo. Vamos ver por que jamais se deve engolir um erro, quando faz sentido falhar rápido e quando recuperar, a diferença entre exceções e valores de erro, como escrever mensagens úteis sem vazar detalhes internos, e como cuidar da limpeza de recursos e da propagação ao longo das camadas.
Erros fazem parte do fluxo normal#
O primeiro ajuste mental é parar de enxergar o erro como algo anormal. Em qualquer sistema real, coisas dão errado o tempo todo, e isso é esperado, não excepcional. A rede cai, o disco enche, um arquivo não existe, um serviço externo demora a responder, um usuário envia dados malformados, uma restrição do banco é violada. Nenhuma dessas situações é um acidente raro; todas são eventos previsíveis que acontecem rotineiramente em produção.
Se essas situações são previsíveis, então o comportamento do sistema diante delas precisa ser projetado, não improvisado. O tratamento de erro deixa de ser um apêndice colado no fim e passa a ser parte do desenho da função desde o começo. Ao escrever qualquer operação que possa falhar, a pergunta "o que acontece quando isto falhar?" tem o mesmo peso que "o que isto faz quando funciona?". As duas respostas juntas é que definem o comportamento completo daquele pedaço de código.
Essa mudança de perspectiva tem um efeito prático imediato. Em vez de um caminho feliz longo e detalhado seguido de um tratamento de erro genérico e apressado, você passa a distribuir a atenção de forma mais equilibrada, considerando cada ponto de falha à medida que ele aparece. O código resultante é mais robusto porque nasceu contando com o fracasso, e não fingindo que ele não vai acontecer. Sistemas maduros não são os que nunca encontram erros; são os que sabem exatamente o que fazer quando os encontram.
Nunca engolir um erro#
Se há uma única regra inegociável no tratamento de erros, é esta: nunca engula um erro em silêncio. Engolir um erro significa capturá-lo e não fazer nada com ele, um bloco de captura vazio, um catch {} que descarta a exceção, um valor de erro que é ignorado e simplesmente jogado fora. É a forma mais direta de transformar uma falha visível e diagnosticável em um bug invisível e traiçoeiro.
O problema do erro engolido é que ele quebra a cadeia de causa e efeito. Quando um erro é descartado, o sistema continua rodando como se nada tivesse acontecido, mas agora ele está em um estado que a lógica não previu. Uma operação que deveria ter falhado prossegue com dados incompletos, um valor que deveria existir está ausente, uma etapa que deveria ter abortado seguiu adiante. As consequências dessa incoerência aparecem mais tarde, longe do ponto onde nasceram, num formato que não guarda nenhuma pista sobre a origem real.
O custo disso, em horas de depuração, é enorme. Um erro tratado de forma visível se manifesta imediatamente, no ponto certo, com informação sobre o que falhou. Um erro engolido se manifesta como um sintoma distante e sem contexto, e a pessoa que investiga precisa percorrer o caminho inteiro de trás para frente até descobrir que, lá atrás, uma exceção foi silenciada. O tempo economizado ao escrever o bloco vazio é pago, com juros altíssimos, por quem depois tem de caçar o fantasma que ele criou.
Silenciar um erro é uma decisão de design, e uma decisão quase sempre errada. Se um erro pode mesmo ser ignorado sem consequência, isso é uma afirmação forte que merece ser documentada explicitamente, com um comentário dizendo por que a falha ali é irrelevante e segura. Um bloco de captura vazio, sem justificativa, não comunica "isto é seguro ignorar"; comunica "alguém não quis lidar com isto", e essa é a origem de incontáveis noites mal dormidas em plantões de produção.
Falhar rápido ou recuperar#
Diante de um erro, existem essencialmente duas atitudes possíveis, e escolher a certa depende do contexto. A primeira é falhar rápido: interromper a operação imediatamente, o mais perto possível do ponto onde o problema foi detectado, e propagar a falha para cima. A segunda é recuperar: lidar com o erro ali mesmo, aplicando uma alternativa, um valor padrão, uma nova tentativa, e permitir que o fluxo continue.
Falhar rápido é a atitude correta na imensa maioria dos casos, principalmente quando o erro indica que uma premissa fundamental foi violada. Se um dado obrigatório está ausente, se uma configuração essencial não foi carregada, se uma invariante do sistema foi quebrada, continuar rodando só vai propagar a corrupção. Abortar cedo, no ponto exato da detecção, mantém o erro perto de sua causa e facilita enormemente o diagnóstico. Um sistema que falha rápido é um sistema que diz a verdade sobre seu próprio estado.
Recuperar é apropriado quando o erro é esperado e há uma resposta sensata e local para ele. Um serviço externo que não respondeu pode justificar uma nova tentativa com espera; um arquivo de cache ausente pode ser recriado; um valor opcional que faltou pode assumir um padrão bem definido. A recuperação legítima é aquela em que você sabe exatamente o que aquele erro significa naquele ponto e tem uma ação correta a tomar. O que não vale é "recuperar" no sentido de fingir que o erro não aconteceu, engolindo-o disfarçado de tratamento.
A escolha entre as duas atitudes depende de quem tem informação para decidir. Frequentemente, o ponto onde o erro ocorre não sabe o que fazer com ele; quem sabe é uma camada acima, que tem o contexto do que a operação inteira significava. Por isso, a regra prática é: recupere onde você tem contexto e uma ação correta; propague quando não tiver. Deixar o erro subir até quem sabe decidir é quase sempre melhor do que tomar uma decisão ruim no lugar errado.
Exceções ou valores de erro#
As linguagens oferecem, em geral, dois modelos para representar falhas, e vale entender as implicações de cada um. No modelo de exceções, um erro interrompe o fluxo normal e "salta" pela pilha de chamadas até encontrar um bloco que o capture. No modelo de valores de erro, uma função que pode falhar retorna explicitamente um valor que representa sucesso ou falha, e o chamador é obrigado a examinar esse valor antes de prosseguir.
Cada abordagem tem virtudes e riscos. As exceções mantêm o caminho feliz limpo, sem verificações a cada passo, mas têm o defeito de tornar o fluxo de erro invisível: olhando a assinatura de uma função, muitas vezes não dá para saber que ela pode falhar nem de que formas. Esse ocultamento facilita justamente o pecado de esquecer de tratar um erro, porque nada na chamada obriga o programador a pensar nele. Exceções não capturadas viajam longe, e a distância entre onde a falha ocorre e onde ela é tratada dificulta o raciocínio.
Os valores de erro seguem a filosofia oposta: a possibilidade de falha fica explícita na assinatura, e o compilador ou a disciplina do time força o chamador a lidar com ela. Isso torna o fluxo de erro visível e local, ao custo de um pouco mais de verbosidade no caminho feliz. Muitas linguagens modernas adotam esse modelo justamente porque ele torna quase impossível ignorar um erro por acidente, transformando o tratamento de falhas em parte obrigatória do contrato de cada função.
Independentemente do modelo que a linguagem impõe, o princípio subjacente é o mesmo: a possibilidade de falha deve ser explícita e o tratamento deve ser consciente. Um bom uso de exceções reserva-as para condições realmente excepcionais e as captura em fronteiras bem definidas; um bom uso de valores de erro trata cada resultado de falha no ponto certo em vez de propagá-lo cegamente. O inimigo comum aos dois modelos é a negligência: o erro que ninguém olhou.
Mensagens úteis, com contexto e sem vazar detalhes#
Um erro só é tão bom quanto a informação que ele carrega. Uma falha que se manifesta como uma mensagem genérica, do tipo "ocorreu um erro", é quase tão inútil quanto um erro engolido, porque não dá a quem investiga nenhuma pista sobre o que aconteceu. Boas mensagens de erro são específicas: dizem o que se tentava fazer, com quais dados, e por que falhou. Elas transformam a depuração de uma caça às cegas em uma leitura direta.
O contexto é o ingrediente que distingue uma mensagem útil de uma inútil. À medida que um erro sobe pelas camadas do sistema, cada camada tem a oportunidade de acrescentar o que sabe: a camada baixa diz "conexão recusada", a camada intermediária acrescenta "ao consultar o usuário de identificador tal", a camada alta completa "ao processar o pedido tal". Esse enriquecimento progressivo constrói uma trilha que leva o investigador do sintoma até a causa sem que ele precise adivinhar. Preservar a causa original ao envolver um erro em outro é essencial: perder o erro de baixo ao substituí-lo por um genérico apaga justamente a informação mais valiosa.
Ao mesmo tempo, há uma distinção crucial entre a mensagem interna e a mensagem exposta ao usuário final ou a um cliente externo. Detalhes internos, nomes de tabelas, trechos de consulta ao banco, caminhos de arquivo, estruturas da aplicação, não devem vazar para fora. Além de confundir quem não conhece o interior do sistema, esses detalhes são informação valiosa para quem quer atacá-lo, revelando a estrutura interna que deveria permanecer opaca. A regra é registrar internamente o máximo de detalhe, para diagnóstico, e devolver externamente o mínimo necessário, uma mensagem genérica e, quando útil, um identificador que permita correlacionar a falha com o registro interno completo.
Esse duplo padrão não é contradição, é defesa em profundidade. Internamente, você quer riqueza total de contexto para depurar; na fronteira externa, você quer discrição para não confundir nem expor. Manter os dois separados, com uma tradução clara entre o erro interno detalhado e a resposta externa comedida, é a marca de um sistema que trata erros de forma profissional.
Limpeza de recursos e propagação#
Um aspecto do tratamento de erros que costuma ser esquecido é a limpeza de recursos. Quando uma operação falha no meio do caminho, ela pode deixar coisas abertas ou inconsistentes: arquivos não fechados, conexões não liberadas, travas não soltas, transações não finalizadas. Se esses recursos não forem devidamente liberados no caminho de erro, o sistema acumula vazamentos que, com o tempo, degradam ou derrubam a aplicação.
Por isso, o caminho de falha precisa cuidar da limpeza com o mesmo rigor do caminho de sucesso. As linguagens oferecem mecanismos para garantir que uma finalização ocorra independentemente de a operação ter dado certo ou errado, e usá-los é obrigatório para qualquer recurso que precise ser liberado. O ponto delicado é a cancel-safety: quando uma operação é interrompida no meio, o sistema não pode ficar num estado inconsistente, com metade de uma mudança aplicada. Ou a operação se completa por inteiro, ou ela se desfaz por inteiro; um meio-termo corrompido é o pior resultado possível.
A propagação correta ao longo das camadas fecha o ciclo. Cada camada do sistema deve decidir conscientemente o que faz com um erro que sobe até ela: recuperar, se tiver contexto e ação; enriquecer com informação e repassar, se não tiver; ou traduzir para a linguagem apropriada da fronteira, se estiver na borda externa. O que nunca deve acontecer é o erro se perder no caminho, seja por ser engolido, seja por ser substituído por outro que apaga a causa original. Um erro bem propagado chega ao topo carregando toda a história de como surgiu.
Vale ainda distinguir erros de domínio de erros de borda. Erros de domínio são aqueles que fazem parte das regras de negócio, um saldo insuficiente, um pedido já cancelado, e devem ser modelados como parte explícita do vocabulário do sistema, tratados como resultados legítimos e não como catástrofes. Erros de borda são os técnicos, de infraestrutura e de fronteira, a rede, o disco, o banco, e pertencem às camadas externas, que os capturam e os traduzem. Manter essa separação clara evita que detalhes técnicos contaminem a lógica de negócio e que regras de negócio se disfarcem de falhas técnicas.
Uma disciplina, não um apêndice#
Tratar erros bem é, no fim, uma disciplina que permeia todo o código, e não uma etapa que se cumpre no final para satisfazer o compilador. Ela começa na mudança de mentalidade que reconhece o erro como parte esperada do fluxo, passa pela recusa absoluta de engolir qualquer falha, e se manifesta em decisões conscientes sobre falhar ou recuperar, sobre o que registrar e o que expor, sobre como limpar recursos e como propagar contexto.
O código que resulta dessa disciplina tem uma qualidade difícil de descrever mas fácil de reconhecer: ele é confiável sob adversidade. Quando algo dá errado, e algo sempre dá, o sistema se comporta de forma previsível, deixa rastros claros, preserva a integridade dos dados e comunica o que aconteceu a quem precisa saber. Essa robustez não aparece no caminho feliz, que qualquer código percorre; ela aparece justamente nos momentos difíceis, e é neles que se mede a real qualidade de um software.
Investir em tratamento de erros nunca é o trabalho mais glamouroso, mas é um dos que mais separam código amador de código profissional. Enquanto o iniciante celebra o caminho feliz e trata a falha como incômodo, o experiente sabe que o valor duradouro de um sistema está na forma como ele lida com o que sai do esperado. Falhar bem, sem engolir exceções, com contexto e sem vazamento, é uma das habilidades mais silenciosamente importantes de quem escreve software para durar.