en
· 4 min de leitura

Error boundaries de ponta a ponta

Error boundary não é um componente React. É uma decisão sobre onde a falha para, tomada em cada camada, do clique até o banco.

fullstackreliability

O React nos deu error boundaries e a maioria dos times parou ali. Um componente lança exceção, um fallback renderiza, o resto da página sobrevive. Isso é uma camada de um sistema que tem seis, e em auditorias descubro que as outras cinco não têm boundary nenhum. Uma rejeição não tratada em um worker de fila derruba o processo. Um webhook que falhou tenta de novo para sempre. Uma Server Action lança erro e o usuário vê uma mensagem genérica sem ideia do que fazer.

Um error boundary é uma decisão sobre onde uma falha para de se propagar e quem é avisado. Essa decisão precisa ser tomada em cada camada, e precisa ser consistente, ou o sistema falha na camada que você esqueceu.

Esperado e inesperado são coisas diferentes

A primeira decisão é a classificação. Um cartão vencido, uma falha de validação, um registro que não existe mais: isso é esperado. Faz parte do domínio, acontece todo dia e deveria ser valor, não exceção. Uma função que pode falhar de forma esperada retorna um result type com um ramo de erro explícito, e quem chama trata no fluxo normal.

Erros inesperados são o banco inacessível, um null onde o tipo dizia que não podia haver, uma API de terceiro retornando HTML em vez de JSON. Esses são lançados, porque ninguém no ponto de chamada consegue fazer algo útil com eles, e precisam subir até um boundary que saiba falhar com segurança.

Misturar os dois é a fonte da maior parte do tratamento de erro ruim que leio. Lançar exceção para cartão recusado significa que todo chamador precisa de um try. Retornar valor para banco fora do ar significa que a queda é engolida em um ramo que loga e segue em frente.

A camada React

No App Router, um arquivo de erro ao lado de um segmento de rota vira o boundary daquele segmento. É um Client Component que recebe o erro e uma função de reset. O layout acima continua funcionando, que é exatamente o que você quer: um widget quebrado na barra lateral não deveria matar o checkout.

Em produção, o framework remove a mensagem dos erros lançados em Server Components e envia um digest no lugar, para não vazar detalhes internos. Isso significa que o boundary não pode mostrar ao usuário o que deu errado, e nem deveria tentar. O trabalho dele é mostrar um caminho: tentar de novo, voltar, falar com o suporte com o digest anexado. A mensagem de verdade fica nos seus logs, indexada por esse digest.

Server Actions são diferentes. São chamadas do cliente, e se lançam exceção, o cliente recebe o mesmo erro sanitizado. Então falhas esperadas em uma action são retornadas, nunca lançadas, como um resultado estruturado que o formulário pode renderizar campo a campo. As inesperadas são lançadas, logadas no servidor com o digest e capturadas pelo boundary mais próximo.

A camada do processo

Abaixo do React existe um processo Node.js, e ele tem os próprios boundaries. Uma promise rejeitada sem tratamento deveria derrubá-lo, de propósito, porque um processo em estado desconhecido servindo requisições é pior do que um restart. O orquestrador reinicia. O health check falha durante o restart, e o load balancer desvia. Isso só funciona se o health check é real e o orquestrador está configurado, e eu verifico os dois antes de confiar em uma política de derrubar no erro.

Workers de fila precisam de um boundary por mensagem. Uma mensagem que falha é retentada com backoff um número fixo de vezes, depois movida para uma dead-letter queue com o erro anexado. Sem isso, uma mensagem envenenada bloqueia a fila para sempre, e já encontrei exatamente essa situação em produção mais de uma vez.

A camada de relato

Todo boundary reporta antes de se recuperar. O boundary do React envia o digest e a rota. A action loga o formato da entrada e o stack. O worker loga o id da mensagem e o número da tentativa. Tudo vai para um lugar só, com um correlation id que liga o clique à query.

Se um boundary engole um erro sem reportar, ele não tratou o erro. Escondeu. A diferença é se o engenheiro de plantão fica sabendo da falha por um dashboard ou por um cliente.

Desenhe os boundaries no quadro, um por camada, com uma seta para onde cada um reporta. Se uma camada não tem seta, é ali que o próximo incidente começa.