You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[types] O único err.message que sobrou no repo torna o fallback pt-BR inalcançável — editar lançamento de devedor exibe o parágrafo em inglês do React em toda falha #122
// components/devedores/EditEntryDialog.tsx:68-76startTransition(async()=>{try{awaitupdateDebtEntry(result.data)toast.success('Lançamento atualizado.')onOpenChange(false)}catch(err){toast.error(errinstanceofError ? err.message : 'Erro ao editar lançamento.')}})
O ramo false do ternário é inalcançável em produção. Erro que atravessa a fronteira de Server Action é reconstruído pelo cliente RSC como um Error genérico (resolveErrorProd, evidência levantada e lida no bundle instalado em #34). err instanceof Error é portanto sempre verdadeiro, e err.message é sempre o mesmo parágrafo:
"An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error."
A string 'Erro ao editar lançamento.' está escrita, versionada, e nunca é exibida. O tipo é verdade sintática e mentira semântica pela segunda vez no mesmo catch: err é Error e .message é string, mas o ternário foi escrito assumindo que instanceof Error distingue "erro da minha action" de "outra coisa" — e ele não distingue nada, porque tudo chega como Error.
Isto não é o achado da #34, e não contradiz o que ela decidiu. A auditoria de 2026-08-13 examinou este mesmo site e o excluiu do escopo da #34, corretamente: a pergunta lá era "esta action deveria devolver ActionResult?", e a resposta é não — as falhas que updateDebtEntry lança hoje ('Lançamento não encontrado' em lib/actions/debtors.ts:205, 'Pessoa não encontrada' em :240, 'Não autorizado' de lib/auth/require-user.ts:6) são todas da classe excepcional que o doc comment de lib/actions/types.ts manda manter como throw. Aquele julgamento continua certo e não é revisitado aqui.
O que ficou sem tratamento é outra coisa: dado que essas falhas devem continuar como throw, o cliente precisa mostrar a mensagem genérica em pt-BR que ele já tem escrita — e hoje não mostra. A correção não é converter a action; é parar de ler err.message.
Verificações feitas (tentativa de falsificar o achado)
É o último site mesmo?grep -rn "err instanceof Error\|error.message" components/ app/ --include="*.tsx" devolve exatamente uma linha. Os outros quatro sites que a [types] Mensagens de erro de Server Actions são mascaradas em produção — 5 componentes exibem err.message que nunca chega ao usuário #34 enumerava foram todos convertidos e hoje leem o retorno da action: FaturaPaymentDialog.tsx:94, SettleChargeDialog.tsx:75, DebtorList.tsx:46 (result.message) e CreditModeSection.tsx:45 (setError(result.message)). Achado de site único — não é multi-site.
A convenção do repo é o oposto, e é esmagadora. O padrão dominante é catch {} + string pt-BR fixa: InvestmentTypeAccordion.tsx:123, InvestmentTypeCard.tsx:133, ui/row-actions.tsx, ui/delete-button.tsx e ~30 outros. Este site é a minoria, não discussão de arquitetura.
Foi decisão deliberada? Não. git log --follow mostra que a linha veio por cópia no rename de EditChargeDialog.tsx → EditEntryDialog.tsx (0a45677, "editar valor e observação de qualquer lançamento"), commit de feature que não menciona contrato de erro. O mesmo commit removeu a regra 'Só é possível editar cobranças em aberto' — que era a única mensagem acionável que este err.message já teve razão de tentar exibir. Sobrou o mecanismo sem o motivo.
Falha do usuário mais provável, verificada no caminho real: o cliente já roda updateDebtEntrySchema.safeParse antes de chamar a action (:61), então ZodError do servidor é improvável. O que sobra e acontece de verdade é sessão expirada (requireUserId lança em require-user.ts:6; a sessão é JWT com maxAge de 24 h, lib/auth.ts:43) e falha de rede. Nos dois casos a ação corretiva é a mesma — recarregar / refazer login — e nos dois o usuário recebe o parágrafo em inglês.
Limitação declarada: este ambiente está sem node_modules, então o resolveErrorProd não foi relido aqui. A evidência é a da [types] Mensagens de erro de Server Actions são mascaradas em produção — 5 componentes exibem err.message que nunca chega ao usuário #34, que o leu no bundle instalado (next@16.2.6); o mascaramento é comportamento upstream do React Server Components, não do Next, e não muda em bump de patch. Quem implementar pode reconfirmar em node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.production.js.
Impacto
Quem edita um lançamento em /devedores/[id] — o dialog é aberto por DebtEntryList.tsx:320, tela renderizada — e esbarra em sessão expirada ou rede instável. Em vez de "Erro ao editar lançamento.", o toast exibe um parágrafo de 220 caracteres em inglês, sobre Server Components e digest, num app que é inteiramente em português.
O usuário não tem como saber o que fazer: a mensagem não diz que a sessão caiu nem sugere recarregar. E como o dialog corretamente não fecha no catch, ele fica com o formulário aberto, reenviando e recebendo o mesmo parágrafo. Não há console.error no caminho e o projeto não tem Sentry, então essa é também a única evidência que a falha deixa — e ela aponta para lugar nenhum.
Em desenvolvimento o problema é invisível: next dev não mascara, e o toast mostra 'Lançamento não encontrado' ou 'Não autorizado', que parecem razoáveis. Só o build de produção troca a mensagem.
Cobertura
Não existe teste que pegue isso, e teste de render não é opção — não há @testing-library/react no projeto, então propor um significaria propor a infra inteira.
O caminho é um gate por string sobre o código-fonte, que é precedente versionado neste repo: __tests__/unit/row-actions.test.ts:18 faz exatamente isso (readFileSync + asserção sobre o conteúdo de components/ui/row-actions.tsx), e __tests__/unit/service-worker-registro.test.ts usa a mesma maquinaria.
Caso proposto — um it() que varre components/**/*.tsx e falha se algum arquivo contiver err.message/error.message dentro de um catch:
it('nenhum componente exibe err.message — a mensagem é mascarada em produção',()=>{constofensores=arquivosTsx().filter((f)=>/catch[\s\S]{0,200}?\b(err|error|e)\s*\.\s*message\b/.test(readFileSync(f,'utf-8')))expect(ofensores).toEqual([])})
A entrada que só a correção certa rejeita: o arquivo EditEntryDialog.tsx como está hoje. O gate falha agora (1 ofensor) e passa depois. E ele descarta as duas correções erradas mais prováveis:
⚠️O gate precisa excluir errors.message/next.message: components/feedback/FeedbackDialog.tsx:36,75,81 usa errors.message como chave do mapa de erros de formulário, que não tem relação nenhuma com isto. A âncora em catch no regex acima já resolve; sem ela o gate nasce falso-positivo.
Proposta
Uma linha, em components/devedores/EditEntryDialog.tsx:73-75:
}catch(err){console.error('[EditEntryDialog] updateDebtEntry falhou',err)toast.error('Erro ao editar lançamento.')}
A string já existe no arquivo — a mudança é só deixar de descartá-la em favor de err.message.
Por que console.error e não só remover o err.message (exigência 6): sem o log, catch que ignora err vira o "handler que engole a exceção sem log nem feedback" que o CLAUDE.md proíbe — a troca resolveria a mensagem e pioraria o diagnóstico. É o mesmo remédio já aplicado em components/ui/row-actions.tsx pela correção da #35, então não é padrão novo: é o padrão do repo chegando ao último arquivo que faltava.
Não usar err.digest como substituto: é hash de mensagem + stack, instável entre builds, e não diz nada ao usuário. E não converter updateDebtEntry para ActionResult — a #34 já examinou e descartou essa rota para esta action, com o motivo registrado.
Custo estimado
P (1 arquivo, + 1 arquivo de teste se o gate entrar).
Onde
components/devedores/EditEntryDialog.tsx:74— último site do repo com esse padrão:Evidência
O ramo
falsedo ternário é inalcançável em produção. Erro que atravessa a fronteira de Server Action é reconstruído pelo cliente RSC como umErrorgenérico (resolveErrorProd, evidência levantada e lida no bundle instalado em #34).err instanceof Erroré portanto sempre verdadeiro, eerr.messageé sempre o mesmo parágrafo:A string
'Erro ao editar lançamento.'está escrita, versionada, e nunca é exibida. O tipo é verdade sintática e mentira semântica pela segunda vez no mesmocatch:erréErrore.messageéstring, mas o ternário foi escrito assumindo queinstanceof Errordistingue "erro da minha action" de "outra coisa" — e ele não distingue nada, porque tudo chega comoError.Isto não é o achado da #34, e não contradiz o que ela decidiu. A auditoria de 2026-08-13 examinou este mesmo site e o excluiu do escopo da #34, corretamente: a pergunta lá era "esta action deveria devolver
ActionResult?", e a resposta é não — as falhas queupdateDebtEntrylança hoje ('Lançamento não encontrado'emlib/actions/debtors.ts:205,'Pessoa não encontrada'em:240,'Não autorizado'delib/auth/require-user.ts:6) são todas da classe excepcional que o doc comment delib/actions/types.tsmanda manter comothrow. Aquele julgamento continua certo e não é revisitado aqui.O que ficou sem tratamento é outra coisa: dado que essas falhas devem continuar como
throw, o cliente precisa mostrar a mensagem genérica em pt-BR que ele já tem escrita — e hoje não mostra. A correção não é converter a action; é parar de lererr.message.Verificações feitas (tentativa de falsificar o achado)
grep -rn "err instanceof Error\|error.message" components/ app/ --include="*.tsx"devolve exatamente uma linha. Os outros quatro sites que a [types] Mensagens de erro de Server Actions são mascaradas em produção — 5 componentes exibemerr.messageque nunca chega ao usuário #34 enumerava foram todos convertidos e hoje leem o retorno da action:FaturaPaymentDialog.tsx:94,SettleChargeDialog.tsx:75,DebtorList.tsx:46(result.message) eCreditModeSection.tsx:45(setError(result.message)). Achado de site único — não é multi-site.catch {}+ string pt-BR fixa:InvestmentTypeAccordion.tsx:123,InvestmentTypeCard.tsx:133,ui/row-actions.tsx,ui/delete-button.tsxe ~30 outros. Este site é a minoria, não discussão de arquitetura.git log --followmostra que a linha veio por cópia no rename deEditChargeDialog.tsx→EditEntryDialog.tsx(0a45677, "editar valor e observação de qualquer lançamento"), commit de feature que não menciona contrato de erro. O mesmo commit removeu a regra'Só é possível editar cobranças em aberto'— que era a única mensagem acionável que esteerr.messagejá teve razão de tentar exibir. Sobrou o mecanismo sem o motivo.updateDebtEntrySchema.safeParseantes de chamar a action (:61), então ZodError do servidor é improvável. O que sobra e acontece de verdade é sessão expirada (requireUserIdlança emrequire-user.ts:6; a sessão é JWT commaxAgede 24 h,lib/auth.ts:43) e falha de rede. Nos dois casos a ação corretiva é a mesma — recarregar / refazer login — e nos dois o usuário recebe o parágrafo em inglês.node_modules, então oresolveErrorProdnão foi relido aqui. A evidência é a da [types] Mensagens de erro de Server Actions são mascaradas em produção — 5 componentes exibemerr.messageque nunca chega ao usuário #34, que o leu no bundle instalado (next@16.2.6); o mascaramento é comportamento upstream do React Server Components, não do Next, e não muda em bump de patch. Quem implementar pode reconfirmar emnode_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.production.js.Impacto
Quem edita um lançamento em
/devedores/[id]— o dialog é aberto porDebtEntryList.tsx:320, tela renderizada — e esbarra em sessão expirada ou rede instável. Em vez de "Erro ao editar lançamento.", o toast exibe um parágrafo de 220 caracteres em inglês, sobre Server Components edigest, num app que é inteiramente em português.O usuário não tem como saber o que fazer: a mensagem não diz que a sessão caiu nem sugere recarregar. E como o dialog corretamente não fecha no
catch, ele fica com o formulário aberto, reenviando e recebendo o mesmo parágrafo. Não háconsole.errorno caminho e o projeto não tem Sentry, então essa é também a única evidência que a falha deixa — e ela aponta para lugar nenhum.Em desenvolvimento o problema é invisível:
next devnão mascara, e o toast mostra'Lançamento não encontrado'ou'Não autorizado', que parecem razoáveis. Só o build de produção troca a mensagem.Cobertura
Não existe teste que pegue isso, e teste de render não é opção — não há
@testing-library/reactno projeto, então propor um significaria propor a infra inteira.O caminho é um gate por string sobre o código-fonte, que é precedente versionado neste repo:
__tests__/unit/row-actions.test.ts:18faz exatamente isso (readFileSync+ asserção sobre o conteúdo decomponents/ui/row-actions.tsx), e__tests__/unit/service-worker-registro.test.tsusa a mesma maquinaria.Caso proposto — um
it()que varrecomponents/**/*.tsxe falha se algum arquivo contivererr.message/error.messagedentro de umcatch:A entrada que só a correção certa rejeita: o arquivo
EditEntryDialog.tsxcomo está hoje. O gate falha agora (1 ofensor) e passa depois. E ele descarta as duas correções erradas mais prováveis:err instanceof Error && err.message !== '' ? ... : fallback— continua lendo.message, gate segue vermelho;updateDebtEntryparaActionResult— passaria o gate, mas contraria o julgamento já registrado na [types] Mensagens de erro de Server Actions são mascaradas em produção — 5 componentes exibemerr.messageque nunca chega ao usuário #34 sobre esta action; por isso a issue pede explicitamente a mudança no cliente, não na action.errors.message/next.message:components/feedback/FeedbackDialog.tsx:36,75,81usaerrors.messagecomo chave do mapa de erros de formulário, que não tem relação nenhuma com isto. A âncora emcatchno regex acima já resolve; sem ela o gate nasce falso-positivo.Proposta
Uma linha, em
components/devedores/EditEntryDialog.tsx:73-75:A string já existe no arquivo — a mudança é só deixar de descartá-la em favor de
err.message.Por que
console.errore não só remover oerr.message(exigência 6): sem o log,catchque ignoraerrvira o "handler que engole a exceção sem log nem feedback" que oCLAUDE.mdproíbe — a troca resolveria a mensagem e pioraria o diagnóstico. É o mesmo remédio já aplicado emcomponents/ui/row-actions.tsxpela correção da #35, então não é padrão novo: é o padrão do repo chegando ao último arquivo que faltava.Não usar
err.digestcomo substituto: é hash de mensagem + stack, instável entre builds, e não diz nada ao usuário. E não converterupdateDebtEntryparaActionResult— a #34 já examinou e descartou essa rota para esta action, com o motivo registrado.Custo estimado
P (1 arquivo, + 1 arquivo de teste se o gate entrar).