Engenharia

A mensagem de erro dizia "não configurado." Estava configurado.

O bug mais caro não é a falha, é a falha que nomeia a causa errada.

ASR

Apollo Space Research

Apollo Space

· 10 min de leitura

Uma requisição morreu às 2h da manhã. O erro no log tinha quatro palavras: integration not configured. Então o engenheiro de plantão fez a coisa óbvia e responsável, abriu as configurações, checou a credencial, digitou de novo, reiniciou o serviço, viu falhar mais uma vez. Checou uma segunda credencial. Leu o guia de setup. Três horas depois, com o sol nascendo, alguém finalmente puxou a chamada bruta e viu a verdade: a integration estava configurada perfeitamente. O provider do outro lado simplesmente deu timeout, e em algum lugar da stack um handler genérico tinha traduzido não consegui alcançá-los em você configurou isso errado.

Ninguém escreveu código ruim naquela noite. O bug era real e estava a dez minutos de ser corrigido. As três horas foram para as quatro palavras que apontavam para o lugar errado.

O bug mais caro não é a falha, é a falha que nomeia a causa errada.

Este post é sobre por que aquelas quatro palavras custaram tão caro, por que quase todo sistema as produz por padrão, e a disciplina que faz um erro descrever o que realmente aconteceu em vez do que alguém adivinhou.

O bug que mente é pior que o bug que quebra

Comece pelas duas falhas que todo engenheiro já encontrou, e note qual você preferiria ter.

A primeira é o crash honesto. Algo cai, o stack trace aponta para a linha exata, e você corrige. É chato, às vezes numa hora terrível, mas não desperdiça você, a falha e sua causa são o mesmo objeto, sentadas no mesmo lugar. Você lê, acredita, terminou.

A segunda é a falha que nomeia a causa errada. O sistema quebra pela razão A e reporta a razão B. Agora seu debugging está mirado num alvo que nunca esteve quebrado. Cada hora que você gasta “consertando” B é pior que desperdiçada, porque no fim a coisa ainda falha e você agora se convenceu de que a parte realmente quebrada está bem, você acabou de checá-la. A mentira não custa só tempo. Ela ativamente desvia você da resposta e deixa você mais confiante no modelo errado do que quando começou.

Um crash custa minutos. Uma falha mal-nomeada custa as horas que você gasta confiando nela.

É por isso que um error handler genérico é tão sedutor e tão perigoso. Parece cuidado. Toda falha é capturada, toda falha ganha uma mensagem arrumadinha, nada vaza um trace feio para o usuário. Mas um handler que captura tudo e rotula com uma de três strings amigáveis jogou fora a única coisa que importava: o que especificamente deu errado. Trocou a verdade bagunçada por uma mentira limpa. O bug mais caro não é a falha, é a falha que nomeia a causa errada, e o tratamento de erro arrumadinho fabrica essas em escala.

Duas pistas a partir de uma falha: um timeout de provider. Na pista da nomeação, um handler genérico rotula como 'não configurado', um humano caça a config por horas, e a correção nunca toca a falha real. Na pista da evidência, a causa capturada diz que a chamada ao provider deu timeout no boundary, o que aponta direto para a correção.

A versão ingênua: captura, nomeia, segue em frente

A forma como a maioria dos sistemas chega aqui não é negligência. É a linha de código mais razoável que você pode escrever.

Uma chamada em algum lugar pode falhar, então você a envolve. O wrapper captura a exception, e agora você tem que dizer alguma coisa para quem estiver downstream. Você não tem tempo, no momento em que escreve o wrapper, de enumerar cada forma que a chamada pode dar errado, a rede, a auth, o rate limit, a resposta malformada, o provider estar fora, o provider estar no ar mas lento. Então você escolhe a causa mais comum em que consegue pensar, escreve uma mensagem para ela, e shipa. Provavelmente é um problema de config. Vamos dizer “não configurado.”

Funciona. Por meses. As primeiras cem vezes que a chamada falha, é mesmo um problema de config, e a mensagem está certa, e todo mundo feliz. Então uma noite a chamada falha pela centésima-primeira razão, um timeout, e a mensagem diz exatamente o que sempre disse, e é mentira, e ninguém sabe, porque a mensagem foi confiável por tanto tempo que ninguém pensa em duvidar dela.

Essa é a armadilha. O handler ingênuo não está errado no dia um. Ele está errado no dia em que o modo de falha muda, e a essa altura todo mundo aprendeu a confiar nele. Você não é punido pelo palpite quando o escreve. Você é punido meses depois, às 2h da manhã, por um engenheiro que não tem razão alguma para suspeitar das palavras na tela.

E o problema mais profundo: o handler tinha a verdade e a jogou fora. No instante em que a exception foi capturada, o sistema sabia que era um timeout, o tipo da exception dizia isso, o tempo decorrido dizia isso, a ausência de qualquer resposta dizia isso. Tudo isso estava em mãos. O handler olhou para uma falha rica e específica e a achatou numa string que alguém digitou semanas antes a partir de um palpite. A perda de informação aconteceu de propósito, em nome de uma mensagem limpa.

Nosso jeito: a falha carrega sua própria evidência

A correção não é “escrever melhores mensagens de erro.” Palpites melhores ainda são palpites. A correção é parar de adivinhar, fazer a falha descrever a si mesma a partir do que realmente aconteceu, não do que alguém previu que aconteceria.

A ideia-chave é simples. Quando uma chamada falha, a coisa que falha sabe mais sobre a falha do que qualquer um que leia sobre ela depois jamais saberá. Ela sabe o que estava tentando fazer, com quem estava falando, o que voltou, e onde parou. Uma mensagem de erro deveria ser a mensageira disso, não um rótulo escolhido com antecedência e carimbado em toda falha que passa.

Então em cada boundary onde uma parte do sistema fala com outra, um provider, um banco de dados, uma tool, outro serviço, nós registramos a causa em vez de nomeá-la. Não “não configurado”, mas: esta chamada, para este provider, com este formato de requisição, não obteve resposta dentro do timeout. A mensagem não é escrita antecipadamente a partir de um palpite. Ela é montada no momento da falha a partir dos fatos da falha. Quem a ler em seguida, e já chegamos em quem é, recebe o que aconteceu, não a velha teoria de alguém sobre o que costuma acontecer.

Uma falha funda na stack é capturada no boundary. O caminho da nomeação a carimba com um rótulo adivinhado e manda o próximo leitor caçar a coisa errada. O caminho da evidência registra o que chamou o quê, o que respondeu, e onde parou, entregando ao leitor a causa verdadeira para agir certo de primeira.

Note o que isso custa e o que não custa. Não custa muito código, capturar a causa que você já capturou é mais barato que inventar uma mensagem para ela. Não vaza feiura para o usuário final, porque a causa rica e a linha amigável voltada ao usuário são duas coisas diferentes; o usuário ainda vê “algo deu errado do nosso lado”, enquanto o sistema vê o timeout. O que custa é o conforto de uma string arrumadinha, pré-escrita. Você abre mão da ilusão de que sabia, antecipadamente, por que as coisas iam quebrar. Em troca, você ganha uma falha que diz a verdade na noite em que o modo de falha é um que você nunca imaginou.

Por que isso importa mais quando o leitor é um agent

Aqui está a parte que transforma uma conveniência de debugging em algo load-bearing. Por quase toda a história do software, o leitor de um erro era um humano. Humanos são leitores indulgentes, uma pessoa pode olhar de soslaio para não configurado, sentir que não cheira bem, puxar a chamada bruta e contornar a mentira. Lento e caro, mas possível, porque um humano traz suspeita externa ao texto.

Um agent lendo o mesmo erro não traz suspeita alguma. Se a mensagem diz não configurado, o agent vai, sensatamente, tentar configurar. Vai checar a credencial, digitar de novo, sugerir que o usuário se reautentique, vai fielmente perseguir exatamente a correção errada, porque não tem razão independente para descrer das palavras que recebeu. Uma falha mal-nomeada não desperdiça só as horas de um humano agora. Ela manda um ator automatizado confiantemente pelo caminho errado, e um ator automatizado vai por esse caminho rápido e em escala.

O que inverte a prioridade completamente. Num sistema onde agents leem falhas e agem sobre elas, a mensagem de erro não é mais uma reflexão diagnóstica tardia que você arruma depois. É uma instrução. A qualidade de cada recuperação que o sistema consegue fazer, abrir o ticket certo, retentar a chamada certa, dizer ao usuário a coisa verdadeira, é limitada por se a falha descreveu a si mesma honestamente no momento em que aconteceu. Lixo entra, lixo confiante sai.

Quando um humano lê uma mentira, você perde horas. Quando um agent lê uma mentira, você perde horas em velocidade de máquina.

É por isso que tratamos o boundary de captura-de-causa como parte da fundação, não do acabamento. Um agent que consegue ler uma falha verdadeira pode roteá-la: ver o timeout, retentar uma vez, e se ainda der timeout, dizer ao usuário o provider está fora agora, que é verdade, em vez de sua integration está mal configurada, que é falso e manda todo mundo, humano e agent, consertar uma coisa que nunca esteve quebrada. A recuperação é só tão boa quanto a mensagem. Então a mensagem tem que ser boa primeiro.

A virada: é sobre respeito por quem debuga em seguida

Para quem é a falha?

Não para o programa, o programa já deu crash; ele não precisa da mensagem. A falha é escrita para a próxima pessoa, ou o próximo agent, que aparece no pior momento possível com o mínimo de contexto possível e tem que descobrir o que está errado. Esse leitor está cansado. São 2h da manhã, ou é o quadragésimo ticket do dia, ou é um agent três passos dentro de uma tarefa sem memória de como o sistema está conectado. Eles vão confiar nas palavras que você deixou. Essa confiança é a transação inteira.

Uma mensagem de erro adivinhada é um pequeno ato de desrespeito para com esse leitor. Ela diz não tive tempo de descobrir por que isso realmente quebra, então aqui está meu melhor palpite, boa sorte. E o leitor paga por esse atalho por inteiro, na hora em que menos pode bancar, enquanto você dorme. Uma causa capturada é o oposto. Ela diz eu capturei isto quando sabia mais sobre o assunto, e anotei o que era de fato verdade, para que você não tivesse que reconstruir do nada. Isso não é uma feature que você instala. É um hábito, a disciplina de dizer a verdade sobre uma falha no único momento em que você tem certeza dela, em nome de alguém que você nunca vai conhecer.

O bug mais caro não é a falha, é a falha que nomeia a causa errada. Tudo que construímos para nos recuperar de falhas, automaticamente e em velocidade, repousa sobre as falhas dizerem a verdade primeiro.


Essa é a disciplina que estamos construindo na Apollo Space: um sistema onde cada falha descreve o que realmente aconteceu, para que o próximo leitor, um colega de time às 2h da manhã ou um agent no meio de uma tarefa, nunca queime uma hora consertando uma parte que nunca esteve quebrada. Se você já perdeu uma noite para quatro palavras que apontavam para o lugar errado, você já sabe qual bug decidimos matar primeiro.

A Apollo cuida da operação repetitiva da sua empresa pro seu time não precisar.

Entre na lista de espera: acesso antecipado, preço de usuário fundador e um lugar na primeira fila enquanto a gente constrói.

Entrar na lista de espera