Leia o tratamento de erros primeiro
Blocos catch são onde um time escreveu o que teme e o que escolheu ignorar. Leia-os antes de qualquer outra coisa.
Se você me desse dez minutos com uma base de código desconhecida e pedisse para prever a próxima queda, eu não leria a arquitetura nem a lógica de negócio principal. Buscaria todo catch, todo rescue, todo except, todo .catch(), e leria em ordem. Tratamento de erro é onde um time escreveu, sem querer, o que temia, o que não entendia e o que decidiu ignorar. É a documentação mais honesta do repositório.
Os quatro tipos de bloco catch
O catch vazio: um erro aconteceu e o sistema finge que não. O loga-e-segue: o erro é registrado e a função retorna como se tivesse dado certo, então quem chamou prossegue com uma mentira. O pega-tudo: qualquer coisa, incluindo bugs no próprio handler, cai no mesmo bloco e é tratada do mesmo jeito. E o relançamento que perde contexto: o erro original é embrulhado num genérico, e quando chega a um log ninguém sabe mais o que falhou. Conto cada tipo. Uma base de código com muitos dos três primeiros é uma base que não faz ideia de em que estado está depois que qualquer coisa dá errado.
O que um erro engolido custa
O dano de um erro ignorado raramente está onde está o catch. Está mais adiante. Um provedor de pagamento dá timeout, o catch loga um aviso e retorna nulo, quem chamou trata nulo como 'não pago', o pedido é cancelado, e o cliente foi cobrado de verdade. Nada quebrou. Cada função fez o que foi escrita para fazer. O bloco catch tomou uma decisão, em nome do negócio, de que timeout significa pagamento falho, e ninguém nunca revisou essa decisão porque eram três linhas dentro de um try.
Quando leio um catch, a pergunta é sempre: o que quem chamou acredita ser verdade depois desta linha, e é verdade? Se a resposta é 'quem chamou acha que deu certo', esse catch é um bug com atraso.
Retries se escondem no mesmo lugar
Logo ao lado do catch normalmente está o retry, e o retry normalmente está errado. Sem número máximo de tentativas. Sem backoff, então a dependência que está falhando é martelada exatamente no momento em que está mais fraca. Sem verificação de idempotência, então o retry de uma escrita que na verdade deu certo cria uma duplicata. Repetindo erros que nunca vão dar certo, como uma falha de validação, junto com os que podem, como uma oscilação de rede. Um retry é uma decisão de fazer a mesma requisição de novo, e merece o mesmo escrutínio da requisição original. Na maior parte das vezes não recebe nenhum.
Os erros que nunca são capturados
A imagem espelhada importa tanto quanto. Procuro toda chamada externa, todo parse, toda escrita no banco, e confiro se alguma coisa captura a falha. Um parse de JSON no corpo de um webhook sem try em volta. Uma chamada ao banco num job em segundo plano sem handler, então o job morre em silêncio e a fila segue. Uma promise sem catch, o que em alguns runtimes significa que o processo encerra e em outros que o erro some. Caminhos não tratados são blocos catch que ninguém escreveu, e o comportamento é o que o runtime decidir.
Como é o bom
Bom tratamento de erro é chato e específico. Capture os erros de que você de fato consegue se recuperar, por tipo, e deixe todo o resto se propagar até uma fronteira que sabe falhar em voz alta. Nessa fronteira, devolva uma resposta honesta sobre o que aconteceu, registre o erro original com contexto e deixe o sistema num estado em que a próxima requisição possa confiar. Todo retry tem limite, backoff e chave de idempotência. Todo job em segundo plano reporta a própria morte. Nada disso é difícil. Só exige que alguém decida que o caminho de falha é parte do produto, e o leia com o mesmo cuidado do caminho de sucesso.