2.20

View in English

2.20 Tratamento de erros e padrões de resiliência

Visão geral e motivação

Todo programa que você escreve vai falhar. Um disco enche, uma rede cai, um serviço esgota o tempo, um chamador passa lixo, uma dependência devolve algo que a documentação nunca mencionou. A pergunta nunca é se a falha acontece. É se o seu código a enfrenta com um plano ou com uma surpresa. O tratamento de erros é o ofício de decidir, linha a linha e função a função, o que o seu código faz quando o mundo não coopera. É a parte menos glamorosa da construção e a que decide, mais que qualquer funcionalidade, se as pessoas confiam no seu sistema.

Este capítulo trata da resiliência no nível do código e dos componentes: as escolhas dentro de uma função, de um módulo ou de uma API. Ele complementa o capítulo 3.5, que cobre a resiliência no nível do sistema (balanceamento de carga, replicação, failover entre serviços). O capítulo 3.5 mantém a plataforma inteira de pé quando uma região apaga. Este capítulo impede que uma única requisição corrompa seus dados ou suma sem deixar rastro. Os dois se reforçam. Um disjuntor na sua arquitetura pouco significa se o código por trás dele engole exceções, e uma função defensiva não pode salvar você se o sistema ao redor não tem redundância. Este capítulo também se apoia no capítulo 2.9 (construção de software), onde tratar erros era uma disciplina entre muitas. Aqui ele se torna o assunto inteiro.

Para equipes grandes, o prêmio é a consistência. Quando centenas de engenheiros tratam erros de centenas de maneiras diferentes, todo serviço vira um quebra-cabeça e todo incidente, uma escavação. Em contextos corporativos, essa inconsistência eleva o custo de cada auditoria e de cada integração. Em sistemas governamentais e de alto risco, o que está em jogo é mais agudo: correção, falha segura e uma trilha de auditoria clara não são funcionalidades que se acrescentam depois, mas propriedades que o sistema precisa ter desde o primeiro commit. Um sistema de benefícios que calcula errado em silêncio, ou um sistema de registros que perde uma falha sem registrá-la, não é meramente defeituoso. É não confiável de um modo que corrói a instituição por trás dele.

Princípios fundamentais

  • Distinga erros, falhas latentes (faults) e falhas (failures) e trate cada um na camada certa.
  • Escolha falhar rápido ou falhar com segurança deliberadamente, por contexto, nunca por acidente.
  • Torne explícito e honesto o contrato de tratamento de erros de toda função e API.
  • Valide nas fronteiras. Confie dentro delas. Defenda-se sem paranoia.
  • Nunca engula um erro em silêncio. Faça-o emergir, envolva-o ou trate-o de propósito.
  • Torne as novas tentativas seguras com idempotência, tempos limite, recuo e jitter.
  • Dê ao caminho de erro a mesma atenção de design que ao caminho feliz.

Recomendações

Distinga erros, falhas latentes e falhas

Um vocabulário desleixado produz um tratamento desleixado, então comece com termos claros. Uma falha latente (fault) é um defeito no sistema: um bug, uma configuração ruim, uma dependência fora do ar. Um erro é o estado interno incorreto que uma falha latente produz: um nulo onde deveria haver um valor, um saldo que já não se concilia. Uma falha (failure) é o que o observador externo vê: a requisição devolve a resposta errada, ou nenhuma. Uma falha latente pode causar muitos erros, e muitos erros podem ser pegos antes que algum se torne uma falha visível. Todo o sentido do tratamento de erros é quebrar essa cadeia, pegar o erro antes que ele vire uma falha que o usuário ou o auditor experimentam.

Esse vocabulário também diz onde agir. As falhas latentes são tratadas na revisão, no teste e na configuração. Os erros são tratados em tempo de execução pelos padrões deste capítulo. As falhas são tratadas pela observabilidade (capítulo 9.2) e pela resiliência no nível do sistema do capítulo 3.5. Quando a sua equipe compartilha essas palavras, as revisões de incidentes ficam mais afiadas: vocês conseguem dizer com precisão onde a cadeia deveria ter sido quebrada e não foi, em vez de discutir o que era “o bug”.

Escolha falhar rápido ou falhar com segurança por contexto

Falhar rápido significa parar no momento em que algo está errado, recusando-se a prosseguir com um estado ruim para que o problema apareça de forma audível e perto da causa. Falhar com segurança significa degradar para um estado conhecido e inofensivo e continuar atendendo o que se consegue com segurança. Nenhum dos dois é universalmente certo, e a habilidade é escolher por contexto. Durante o desenvolvimento e nas fronteiras internas, falhar rápido é seu amigo: um programa que para diante de um invariante violado lhe dá um rastreamento de pilha curto em vez de um longo mistério. Em produção, nas bordas de um sistema voltado ao usuário, falhar com segurança muitas vezes vence: um painel de recomendações que não devolve nada é melhor que uma página de pagamento que não carrega.

Decida isso deliberadamente para cada fronteira e escreva a decisão. Um componente de controle de voo ou de dispositivo médico falha com segurança para um estado definido porque continuar com dados corrompidos poderia machucar alguém. Um lançamento contábil falha rápido porque lançar uma entrada errada é pior que não lançar nenhuma. O pareamento errado é perigoso nas duas direções: falhar com segurança onde se precisava falhar rápido esconde corrupção, e falhar rápido onde se precisava falhar com segurança transforma um soluço cosmético numa queda de serviço.

Escolha o seu mecanismo de sinalização de erros e use-o de forma consistente

As linguagens oferecem duas grandes formas de sinalizar que algo deu errado. O tratamento de exceções lança um objeto pela pilha de chamadas até algum tratador pegá-lo, separando o caminho de erro da lógica principal. A alternativa são valores de erro explícitos: a função devolve tanto um resultado quanto um erro, e o chamador precisa inspecionar os dois. Muitas linguagens modernas formalizam a segunda com um tipo Result, muitas vezes chamado de Result ou Either, que obriga o chamador a desembrulhar um sucesso ou uma falha antes de usar o valor. Cada abordagem tem um custo. As exceções mantêm limpo o caminho feliz, mas podem esconder o fluxo de controle e tentar os desenvolvedores a blocos que pegam tudo e apagam informação. Os resultados explícitos tornam toda falha visível na assinatura de tipo, mas acrescentam cerimônia e podem ser ignorados se a linguagem não força a verificação.

A resposta certa trata menos de qual mecanismo e mais de consistência e honestidade. Escolha o idioma que a sua linguagem e o seu ecossistema favorecem e aplique-o uniformemente nos seus serviços para que um leitor sempre saiba como a falha viaja. Reserve as exceções para condições genuinamente excepcionais, não para fluxo de controle comum como “usuário não encontrado”, que é melhor modelado como um resultado normal. Seja qual for a sua escolha, nunca deixe uma falha ficar invisível: um valor de erro não verificado é tão perigoso quanto um bloco catch vazio. Numa grande base de código, uma convenção escrita mais um linter que sinaliza erros ignorados vence a preferência de qualquer indivíduo.

Torne explícito o contrato de tratamento de erros

Toda função e toda API tem um contrato de tratamento de erros, quer alguém o tenha escrito ou não. Ele responde: o que pode dar errado aqui, como você ficará sabendo e o que é garantido sobre o estado quando isso acontece? Torne esse contrato explícito. Documente quais erros uma função pode devolver ou lançar, distinga os erros recuperáveis (o chamador pode sensatamente tentar de novo ou recorrer a uma alternativa) dos irrecuperáveis (o chamador não consegue consertar isso e deve propagar ou abortar) e declare se a função deixa o estado inalterado em caso de falha. Esta última propriedade, às vezes chamada de garantia forte de exceção, significa que uma chamada que falhou é como se nunca tivesse acontecido, que é exatamente o que permite ao chamador tentar de novo com segurança.

Para uma API pública ou entre equipes, esse contrato faz parte da interface, tão real quanto os tipos dos parâmetros. Projete uma pequena taxonomia estável de erros: um conjunto limitado de categorias como erro de validação, não encontrado, conflito, não autorizado, dependência indisponível e erro interno. Os chamadores então podem ramificar pela categoria sem analisar cadeias de texto. Uma taxonomia clara torna o tratamento de erros composável entre muitos serviços e torna as falhas auditáveis, porque toda falha mapeia para um tipo conhecido e nomeado.

Valide nas fronteiras e defenda-se sem paranoia

Trate os dados que cruzam uma fronteira de confiança (uma requisição de rede, um arquivo, uma entrada de usuário, uma mensagem de outro serviço) como hostis até serem validados e valide-os na fronteira, uma vez, com minúcia. Isso é programação defensiva aplicada com discernimento. Dentro de um módulo cujas entradas você já validou, verificações redundantes em cada linha escondem a lógica e suprimem as próprias falhas que você gostaria de ver. A disciplina é: defenda-se com firmeza nas bordas, confie dentro delas. Valide estrutura, faixas e invariantes onde os dados entram, converta-os em tipos que tornem irrepresentáveis os estados ilegais e deixe que o código interior presuma que trabalha com dados limpos.

A paranoia tem um custo real. O código sufocado em verificações de nulo e ramos defensivos é mais difícil de ler e, pior, muitas vezes transforma uma falha clara num dar de ombros silencioso, devolvendo um valor padrão onde deveria ter soado um alarme. A defensividade que mascara bugs não é segurança, é adiamento.

Torne as novas tentativas seguras, limitadas e educadas

Muitas falhas latentes são transitórias: um soluço momentâneo de rede, um serviço reiniciando, uma breve contenção de trava. Em qualquer sistema distribuído (capítulo 3.3), essas falhas parciais são o caso normal e não a exceção. Tentar de novo é a resposta natural, mas um laço ingênuo de novas tentativas é uma arma carregada. Primeiro, torne idempotente a operação que você repete, ou seja, executá-la duas vezes tem o mesmo efeito que executá-la uma vez. Sem idempotência, uma nova tentativa depois de um tempo esgotado pode cobrar um cartão duas vezes ou criar dois registros, porque você não consegue saber se a primeira tentativa falhou ou se apenas sua confirmação se perdeu. Use chaves de idempotência para as escritas, de modo que o receptor possa reconhecer e deduplicar uma repetição.

Segundo, ponha um tempo limite em toda chamada remota para que uma dependência travada não trave você. Terceiro, espace as novas tentativas com recuo exponencial, dobrando a espera depois de cada tentativa, e acrescente jitter (um pequeno atraso aleatório) para que mil clientes se recuperando ao mesmo tempo não se sincronizem numa debandada que derrube de novo o serviço em recuperação. Quarto, limite o número de novas tentativas e o tempo total e depois desista com elegância. Novas tentativas sem limites, recuo, jitter e idempotência são uma das formas mais comuns de um pequeno soluço virar uma queda autoinfligida.

Acrescente disjuntores, anteparas e degradação graciosa no código

Quando uma dependência está genuinamente fora do ar, tentar de novo só desperdiça esforço e aprofunda o buraco. Um disjuntor observa a taxa de falha das chamadas a uma dependência e, quando as falhas cruzam um limite, “abre” para falhar imediatamente durante um período de resfriamento em vez de esperar por chamadas condenadas. Depois do resfriamento ele deixa passar uma chamada de teste e fecha de novo se a dependência se recuperou. Isso protege tanto os seus chamadores (falhas rápidas e previsíveis em vez de tempos limite empilhados) quanto a dependência em dificuldade (espaço para respirar e se recuperar). O padrão de anteparas, que leva o nome dos compartimentos estanques de um navio, isola recursos para que uma dependência saturada não consuma todas as threads ou conexões e afunde o processo inteiro: você dá a cada dependência o seu próprio conjunto limitado.

Esses padrões se combinam com a degradação graciosa no nível do código: quando uma dependência não essencial está indisponível, devolva um resultado reduzido mas útil em vez de um erro. Mostre dados em cache com uma nota de obsolescência, esconda o painel de personalização, ponha a escrita em fila para depois. Este é o complemento local da resiliência no nível do sistema do capítulo 3.5: a arquitetura fornece redundância entre máquinas, e o seu código fornece um comportamento sensato quando uma peça está faltando.

Envolva os erros com contexto e nunca os engula

Um erro que diz “conexão recusada” dez camadas acima de onde aconteceu é quase inútil. À medida que um erro se propaga, envolva-o com contexto: o que você estava tentando fazer, qual entidade ou requisição, qual dependência, preservando a causa original para que a raiz não se perca. Boas linguagens e bibliotecas apoiam diretamente esse encadeamento de erros. O objetivo é que uma única linha de log diga à pessoa engenheira de sobreaviso o que falhou, durante qual operação, para qual entrada. Essa é a matéria-prima da observabilidade do capítulo 9.2 e da depuração do capítulo 2.15.

O pecado capital é engolir um erro: um bloco catch vazio, um valor de retorno ignorado, um catch que registra em nível de depuração e continua como se nada tivesse acontecido. Um erro engolido não desaparece: ele reemerge depois como dados corrompidos ou um defeito inexplicável, agora desligado da causa. Todo erro deve ter um de três destinos: tratá-lo (recuperar ou degradar), envolvê-lo e propagá-lo ou, no topo da pilha, registrá-lo com contexto completo e falhar. Se você pega um erro e não faz nenhuma dessas coisas, escolheu esconder de si mesmo no futuro um incidente futuro.

Compromissos: prós e contras

AbordagemPrósContras
ExceçõesCaminho feliz limpo. Difíceis de ignorar se não verificadasFluxo de controle oculto. Tentam ao apagamento por captura geral
Valores de erro explícitos / tipos ResultFalha visível na assinatura. Obrigam a tratarMais cerimônia. Podem ser ignorados sem imposição
Falhar rápidoFaz os bugs emergirem de forma audível, perto da causaMá experiência do usuário se usado na borda
Falhar com segurançaContinua atendendo. Protege usuários e dadosPode mascarar corrupção se usado onde era preciso falhar rápido
Novas tentativas com recuoAtravessam falhas latentes transitórias automaticamenteAmplificam a carga e as escritas duplas sem idempotência
DisjuntorFalhas rápidas. Deixa as dependências se recuperaremEstado adicional e ajuste. Pode mascarar um problema persistente
Validação defensiva nas fronteirasPega dados ruins cedo, uma vez, de forma audívelEm excesso, atravanca a lógica e esconde falhas reais

A tensão central é entre visibilidade e ruído. Trate os erros de forma silenciosa demais e você esconde os problemas até ficarem caros. Trate-os de forma barulhenta demais e em toda parte e você afoga o sinal em cerimônia e mascara as falhas que importam. Resolva isso por localização e intenção. Seja barulhento e estrito nas fronteiras, onde entram dados ruins e falhas de dependência. Seja quieto e confiante no interior, onde as entradas já estão limpas. Decida falhar rápido versus falhar com segurança por fronteira e escreva a decisão. O objetivo é um código em que toda falha tem exatamente um dono claro e um destino claro, e nada cai em silêncio pelas frestas.

Perguntas para discutir com sua equipe

  1. Temos uma taxonomia de erros e uma convenção de tratamento compartilhadas entre os nossos serviços, ou cada equipe improvisa? Numa equipe grande, esta é a diferença entre falhas que se compõem e falhas que confundem. Quando um serviço devolve HTTP 500 para um problema de validação, outro lança uma exceção tipada e um terceiro devolve nulo, toda integração vira uma negociação e todo incidente um exercício de tradução. Leve exemplos da mesma falha lógica, digamos “registro não encontrado”, como aparece em três dos seus serviços e veja quão diferentemente ela é sinalizada. A resposta deve virar um padrão escrito: um conjunto limitado de categorias de erro, uma forma consistente de sinalizá-las e um linter ou lista de verificação de revisão que as imponha. A consistência aqui se paga em toda integração, auditoria e turno de sobreaviso futuros.

  2. Para cada fronteira crítica, escolhemos de propósito falhar rápido ou falhar com segurança, e o código corresponde a essa escolha? A maioria das equipes nunca tomou essa decisão explicitamente, o que significa que foi tomada por elas por quem escreveu o código primeiro, e de forma inconsistente. As considerações concorrentes são reais: falhar com segurança mantém os usuários atendidos mas pode deixar a corrupção se espalhar, enquanto falhar rápido protege os dados mas pode transformar uma pequena queda de dependência numa falha visível. Leve seu histórico de incidentes e pergunte, para os piores, se o código falhou do jeito que vocês teriam escolhido se perguntados de antemão. A evidência que você quer é um mapa das suas fronteiras com um rótulo deliberado em cada uma, especialmente onde há dinheiro, segurança ou registros de cidadãos. Onde o rótulo e o código discordam, vocês encontraram a próxima correção.

  3. Quando foi a última vez que exercitamos um caminho de erro de propósito, e ele se comportou como projetado? O caminho de erro costuma ser o código menos testado que você possui, e ainda assim é onde a confiança é ganha ou perdida, e “falhamos com segurança” é uma afirmação que você não consegue sustentar se nunca viu isso acontecer. Um laço de novas tentativas sem idempotência, um disjuntor com o limite errado, uma exceção engolida num ramo raramente atingido: tudo isso se esconde até um incidente real encontrá-los por você. Leve os resultados de injetar deliberadamente falhas (uma dependência derrubada, um tempo limite provocado, uma carga útil malformada) num ambiente realista. A ação que decorre é tornar rotineira a injeção de falhas, de modo que o comportamento de recuperação, de degradação e de falha segura seja verificado continuamente e não esperado. Todo caminho de erro que vocês nunca dispararam é uma promessa que não foi testada.

  4. Quais das nossas operações de escrita são idempotentes, e onde uma nova tentativa depois de uma confirmação perdida duplicaria um efeito do mundo real, como um pagamento ou um registro? Tentar de novo é o reflexo de resiliência mais comum e, feito sem cuidado, a forma mais comum de um soluço transitório virar dinheiro ou dados duplicados. Numa equipe grande, a lógica de novas tentativas muitas vezes vive em clientes compartilhados, em middleware e em serviços individuais ao mesmo tempo, de modo que uma única escrita pode ser repetida em várias camadas sem que ninguém seja dono do comportamento total. A atração concorrente é que as chaves de idempotência, a deduplicação e os resultados de requisição armazenados acrescentam armazenamento e código, e as equipes sob pressão de entrega os pulam para escritas que erroneamente presumem seguras. Leve um inventário das suas escritas visíveis externamente, cada uma marcada quanto a se carrega uma chave de idempotência e como o receptor reconhece e deduplica uma repetição. Em contextos corporativos e governamentais, sinalizem primeiro as que movem dinheiro ou alteram o registro de um cidadão, porque um pagamento duplo ou um benefício duplicado é uma constatação de auditoria e às vezes uma exposição jurídica, e não meramente um defeito.

  5. Os nossos tempos limite, disjuntores e anteparas vêm de uma biblioteca compartilhada e testada, ou cada equipe os cria à mão? Esses padrões são fáceis de descrever e fáceis de errar de forma sutil: um tempo limite ausente, um limite de disjuntor que nunca dispara, um conjunto de conexões dimensionado de modo que uma dependência lenta deixa todo o processo faminto. Quando cada equipe os reimplementa, vocês acumulam muitas cópias levemente quebradas e nenhum lugar único para corrigir uma falha depois de encontrá-la. A consideração concorrente é que uma biblioteca compartilhada impõe uma interface comum e uma cadência de atualização, e equipes com ambientes de execução incomuns ou necessidades de latência podem se incomodar ou contorná-la. Leve um levantamento de quantas implementações distintas de nova tentativa e de disjuntor de fato rodam em produção e quais serviços ainda não têm nenhum tempo limite nas suas chamadas de saída. Para uma grande empresa ou agência, uma biblioteca compartilhada validada também dá a revisores de segurança e a auditores um único componente a certificar em vez de dezenas, o que reduz o custo de cada revisão.

  6. Se um incidente tivesse acontecido na noite passada, qualquer pessoa de sobreaviso conseguiria rastreá-lo a partir de uma única linha de log, e um auditor conseguiria depois ver toda falha que o sistema registrou? Um erro envolvido, categorizado e bem registrado é a diferença entre um diagnóstico de dez minutos e uma escavação à meia-noite, e um erro engolido é um incidente futuro que você escondeu de si mesmo. Numa equipe grande, as falhas cruzam muitos saltos entre serviços, então o valor vem de contexto consistente e de identificadores de correlação que sobrevivem a esses saltos, não da diligência de qualquer equipe isolada. A tensão concorrente é custo e ruído: registre tudo e você afoga o sinal e paga para armazená-lo. Registre pouco e não consegue reconstruir o que aconteceu. Leve uma falha real recente e percorra seu rastro de ponta a ponta, anotando cada salto em que o contexto foi perdido ou um erro foi pego e descartado. Em sistemas regulamentados e governamentais, trate isso como uma propriedade de conformidade, porque uma falha não auditável, ou uma decisão que você não consegue explicar anos depois, é uma exposição jurídica e não meramente uma lacuna operacional.

Perspectiva por setor

Startup. Com um punhado de engenheiros e nenhuma pista sobrando, gaste o seu orçamento de tratamento de erros onde uma falha custa um cliente ou os seus dados: ponha um tempo limite em toda chamada de saída, torne idempotentes as escritas que movem dinheiro e acrescente uma regra de lint contra erros ignorados. Pule o framework elaborado: um tipo Result nas funções centrais e a degradação graciosa nas dependências não críticas compram a maior parte da segurança por alguns dias de trabalho. Falhe rápido no desenvolvimento para que os bugs surjam de forma audível e resista a criar à mão um disjuntor antes de ter de fato uma dependência que o justifique.

Pequena empresa. Sem especialista em resiliência na equipe e com orçamento apertado, apoie-se no que a sua linguagem, o seu framework e o seu provedor de nuvem já oferecem em vez de construir padrões do zero: filas gerenciadas, novas tentativas do lado do provedor e tempos limite de bibliotecas cobrem mais do que a maioria das equipes espera. Enquadre a decisão como comprar versus construir e compre sempre que uma dependência madura trata por você de novas tentativas, recuo e idempotência. Concentre a sua escassa atenção nas uma ou duas fronteiras em que uma transação errada ou perdida realmente doeria e garanta que elas falhem com segurança e deixem rastro.

Grande empresa. Entre muitas equipes, o prêmio é a consistência: uma taxonomia compartilhada de erros, uma biblioteca comum para tempos limite, novas tentativas, disjuntores e anteparas e um linter e uma lista de verificação de revisão que as imponham no pipeline. Alimente cada erro numa plataforma unificada de observabilidade com identificadores de correlação para que uma falha seja rastreável entre saltos de serviço e padronize as decisões de falhar rápido versus falhar com segurança por fronteira, de modo que as auditorias encontrem um padrão documentado e defensável e não um amontoado de hábitos locais. Governe a biblioteca compartilhada como um produto de verdade, porque uma falha corrigida ali é uma falha corrigida em toda parte.

Governo. Correção, falha segura e uma trilha de auditoria durável são obrigações, não preferências. Falhe rápido diante de qualquer invariante violado que toque dinheiro ou elegibilidade, valide toda entrada voltada ao cidadão na fronteira e escreva cada falha num log imutável com contexto suficiente para que uma decisão possa ser explicada e revisada anos depois. A contratação e as longas vidas dos sistemas significam que os contratos de erro devem ser documentados para que os servidores públicos consigam manter o código muito depois de os autores originais terem ido embora, e qualquer componente de fornecedor deve expor seu comportamento de falha em vez de escondê-lo atrás de uma interface opaca.

Exemplos

Startup. Uma startup de quatro pessoas entrega um aplicativo que chama um provedor de pagamentos de terceiros e um serviço de e-mail. No início acrescentam um laço ingênuo de novas tentativas e logo cobram um cliente em dobro quando um tempo esgotado mascara uma cobrança bem-sucedida. A correção ensina a lição: acrescentam chaves de idempotência a toda escrita, põem um tempo limite em toda chamada de saída e passam para o recuo exponencial com jitter. Adotam um tipo Result para as funções centrais de serviço, de modo que a falha apareça na assinatura, e uma regra de lint sinaliza qualquer erro ignorado. Quando o envio de e-mail falha, o pagamento se degrada com elegância pondo a mensagem em fila em vez de bloquear a venda. A disciplina custa alguns dias e os poupa de uma classe de incidentes que teria custado muito mais em reembolsos e confiança.

Grande empresa. Uma empresa global de logística opera centenas de serviços e padroniza o tratamento de erros em todos eles. Todo serviço mapeia as falhas para uma taxonomia compartilhada (validação, não encontrado, conflito, dependência indisponível, interno), de modo que os chamadores ramificam pela categoria em vez de analisar mensagens. Uma biblioteca comum fornece disjuntores, novas tentativas limitadas com recuo e jitter e conjuntos de conexões com anteparas, de modo que ninguém os cria à mão do jeito errado. Todo erro é registrado com contexto de correlação que alimenta a plataforma de observabilidade do capítulo 9.2, de modo que uma pessoa de sobreaviso consegue rastrear uma falha entre saltos de serviço a partir de uma única linha. Como o padrão é uniforme e imposto no pipeline, os engenheiros transitam com confiança por serviços desconhecidos e os auditores podem ver que toda falha é registrada, categorizada e rastreável.

Governo. Uma agência nacional de benefícios constrói um sistema de elegibilidade e de pagamento em que uma resposta errada pode negar a alguém o dinheiro do aluguel ou pagar a mais do erário público. A correção e a falha segura são inegociáveis, então o código falha rápido diante de qualquer invariante financeiro violado: um cálculo que não consegue se conciliar se recusa a lançar em vez de lançar um valor errado. Toda entrada voltada ao cidadão é validada na fronteira, e os estados ilegais são tornados irrepresentáveis nos tipos de domínio. Cada falha é escrita num log de auditoria imutável com contexto completo, satisfazendo a exigência legal de que as decisões sejam explicáveis e revisáveis anos depois. Onde uma dependência não crítica, como a pré-visualização de documentos, está fora do ar, o sistema se degrada com elegância para que um atendente ainda consiga processar o pedido. Os novos servidores públicos herdam código cujos contratos de erro são documentados, de modo que conseguem mantê-lo com segurança muito depois de os autores originais terem seguido adiante.

Justificativa de negócio: motivações, ROI e TCO

O retorno de um tratamento de erros disciplinado aparece como menos incidentes, incidentes mais curtos e mais baratos. A maioria das quedas de produção não é exótica: remonta a uma exceção engolida, um tempo limite ausente, uma tempestade de novas tentativas ou uma fronteira que confiou em dados que deveria ter validado. Cada uma é evitável com os padrões daqui, e cada incidente evitado poupa não só o custo direto da indisponibilidade, mas os custos cumulativos de resposta de emergência, perda de clientes e investigação. Como um erro envolvido e bem registrado pode ser diagnosticado em minutos e não em horas, o tempo médio de recuperação cai, e a taxa de falha de mudanças cai junto à medida que os engenheiros deixam de temer o caminho de erro.

O custo de adoção é modesto e em grande parte único. Você escreve uma taxonomia de erros, fornece uma biblioteca compartilhada para novas tentativas e disjuntores para que as equipes não os reinventem mal, acrescenta regras de lint contra erros ignorados e constrói o hábito da injeção de falhas. O custo da negligência se acumula em silêncio: erros engolidos se acumulam em dados corrompidos caros de desfazer, e o tratamento inconsistente multiplica o custo de cada integração e de cada auditoria. Em contextos regulamentados e governamentais, uma falha não auditável é uma exposição de conformidade e jurídica, não só um problema de engenharia. Para defender o caso junto à liderança, ligue a disciplina de tratamento de erros às métricas que ela já acompanha: frequência de incidentes, tempo médio de recuperação, taxa de falha de mudanças e constatações de auditoria.

Antipadrões e armadilhas

  • Engolir em silêncio: blocos catch vazios e valores de retorno ignorados que transformam uma falha num mistério tardio e desligado.
  • Apagamento por captura geral: um catch amplo que registra uma mensagem genérica e descarta o erro original e o seu contexto.
  • Nova tentativa sem idempotência: reexecutar escritas não idempotentes depois de um tempo esgotado, cobrando em dobro ou duplicando registros.
  • Tempestades de novas tentativas: sem recuo, sem jitter e sem teto, de modo que os clientes se sincronizam e derrubam de novo uma dependência em recuperação.
  • Sem tempos limite: chamadas remotas sem limite que deixam uma dependência travada esgotar as threads e congelar o processo inteiro.
  • Exceções como fluxo de controle: lançar e pegar para desfechos comuns como “não encontrado”, escondendo lógica e deixando o código lento.
  • Paranoia defensiva: verificações em cada linha que enterram a lógica e convertem falhas reais em valores padrão silenciosos.
  • Erros em texto cru: chamadores analisando o texto da mensagem de erro porque não há uma taxonomia estável e categorizada em que ramificar.
  • Falhar com segurança onde era preciso falhar rápido: continuar com estado corrompido num sistema em que uma resposta errada é pior que nenhuma.

Modelo de maturidade

  • Nível 1, Iniciar: O tratamento de erros é ad hoc e reativo, decidido por desenvolvedor. Blocos catch vazios e retornos ignorados são comuns, as novas tentativas são ingênuas, os tempos limite faltam e as falhas surgem como dados corrompidos ou defeitos misteriosos, sem log consistente.
  • Nível 2, Desenvolver: As equipes adotam práticas básicas, mas de forma inconsistente. Os erros são registrados com algum contexto, engolir de forma óbvia é desencorajado na revisão e existem tempos limite e novas tentativas simples, mas as convenções variam entre serviços, a idempotência é irregular e o caminho de erro raramente é testado.
  • Nível 3, Padronizar: Uma taxonomia de erros e uma convenção de tratamento compartilhadas são documentadas e impostas em toda a organização. A validação de fronteira, as novas tentativas idempotentes com recuo e jitter, os disjuntores, as anteparas e o envolvimento de erros são padrão, fornecidos por bibliotecas comuns, e todo erro alimenta um pipeline unificado de observabilidade.
  • Nível 4, Gerenciar: O comportamento de tratamento de erros é medido em relação a linhas de base e controlado com dados. As taxas de novas tentativas, os disparos de disjuntores, as contagens de tempos limite, as constatações de erros engolidos pela análise estática, o tempo médio de recuperação e a taxa de falha de mudanças são acompanhados por serviço. Os limites dos disjuntores e os tempos limite são ajustados a partir de dados observados de latência e de falha e não adivinhados. A injeção de falhas roda em agenda. E as equipes revisam essas métricas para pegar regressões e manter cada escolha de falhar rápido ou com segurança sujeita a evidências.
  • Nível 5, Orquestrar: A resiliência é integrada ao planejamento de entrega e de risco e continuamente melhorada. A taxonomia, as bibliotecas compartilhadas e os padrões evoluem a partir de cada incidente, os experimentos de caos e de injeção de falhas são rotineiros e a organização adapta os tempos limite, os limites de disjuntores, as estratégias de degradação e as decisões de fronteira conforme o tráfego, as dependências e o quadro de riscos mudam.

Ideias para discussão

  1. Onde, na sua base de código, um erro é engolido hoje, e como você saberia se estiver errado ao achar que não é?
  2. Quais das suas operações de escrita são idempotentes, e quais seriam executadas em dobro se uma nova tentativa disparasse depois de uma confirmação perdida?
  3. “Usuário não encontrado” deve ser uma exceção, um valor de erro ou um resultado normal, e a sua equipe responde a isso de forma consistente?
  4. Qual é a sua regra real sobre onde a validação acontece, e você consegue apontar uma fronteira que confia em dados em que não deveria?
  5. Como você decide o limite e o resfriamento de um disjuntor, e como saberia que as configurações atuais estão erradas?
  6. Se um auditor pedisse para ver toda falha que o seu sistema sofreu no mês passado, você conseguiria produzi-la, categorizada e com contexto?

Principais conclusões

  • Distinga falhas latentes, erros e falhas e quebre a cadeia antes que um erro interno vire uma falha visível.
  • Escolha falhar rápido ou falhar com segurança deliberadamente por fronteira e torne explícito o contrato de tratamento de erros de cada função.
  • Valide com firmeza nas fronteiras de confiança e confie dentro delas. A defensividade que mascara falhas é adiamento, não segurança.
  • Torne as novas tentativas seguras com idempotência, tempos limite, recuo exponencial e jitter e acrescente disjuntores e degradação graciosa no código.
  • Envolva os erros com contexto, alimente a observabilidade e nunca os engula. Todo erro deve ser tratado, propagado, ou registrado e feito emergir.

Referências e leitura complementar

  • Michael T. Nygard, Release It! Design and Deploy Production-Ready Software
  • Andrew Hunt and David Thomas, The Pragmatic Programmer
  • Steve McConnell, Code Complete: A Practical Handbook of Software Construction
  • Betsy Beyer, Chris Jones, Jennifer Petoff, and Niall Richard Murphy (eds.), Site Reliability Engineering: How Google Runs Production Systems
  • Marc Brooker, “Timeouts, Retries, and Backoff with Jitter,” Amazon Builders’ Library
  • Martin Fowler, “CircuitBreaker,” martinfowler.com
  • Nassim Nicholas Taleb, Antifragile: Things That Gain from Disorder