O problema do rethrow sem contexto
Quando você captura um erro e relança outro no JavaScript, perde o contexto original. Isso transforma um bug de 5 minutos em uma sessão de debugging de 2 horas porque o stack trace do erro original some.
Esse problema e mais comum do que parece. Qualquer camada de serviço que trate erros de banco de dados, API externa ou rede e candidata a esse anti-pattern. Você loga um erro genérico e perde a causa raiz que estava no erro original.
A propriedade Error.cause, disponível nativamente desde o Node.js 16.9 e nos browsers modernos, resolve esse problema de forma elegante e sem bibliotecas adicionais.
Como o Error.cause funciona
O Error.cause permite anexar o erro original como propriedade cause do novo erro. Isso preserva toda a cadeia de erros: o erro atual, o erro que o causou, e qualquer erro que causou esse, formando uma cadeia rastreavel.
A sintaxe e simples: passe um objeto de opcoes como segundo argumento para o construtor de Error, com a propriedade cause apontando para o erro original.
O Node.js e os browsers modernos exibem automaticamente a propriedade cause no stack trace, tornando o debugging muito mais rápido sem nenhuma configuração adicional.
O Error.cause esta disponível no Node.js desde a versão 16.9 (agosto 2021) e em todos os browsers modernos. Se você esta no Node 18+, pode usar sem preocupação de compatibilidade.
Principais recursos do Error.cause
O Error.cause oferece alguns benefícios práticos:
- Cadeia de erros preservada: o erro original fica acessível via
err.causeem qualquer nível de tratamento. - Stack trace completo: ferramentas de logging como Sentry e Datadog já exibem a cadeia de cause automaticamente.
- Zero dependência: e uma propriedade nativa do JavaScript, sem necessidade de bibliotecas.
- TypeScript suporte nativo: o tipo ErrorOptions inclui cause desde o TypeScript 4.6.
O Error.cause não substitui boas mensagens de erro. Use cause para preservar o erro original E escreva uma mensagem clara no erro novo que explique o que estava tentando fazer quando o erro aconteceu.
Como começar: antes e depois do Error.cause
Veja a diferença entre o código sem e com Error.cause:
Antes (sem contexto):
// Anti-pattern: perde o erro original
async function buscarUsuario(id) {
try {
const resultado = await db.query("SELECT * FROM users WHERE id = ?", [id]);
return resultado;
} catch (err) {
throw new Error("Falha ao buscar usuário"); // err original perdido!
}
}Depois (com Error.cause):
// Com Error.cause: contexto preservado
async function buscarUsuario(id) {
try {
const resultado = await db.query("SELECT * FROM users WHERE id = ?", [id]);
return resultado;
} catch (err) {
throw new Error("Falha ao buscar usuário com id " + id, { cause: err });
}
}
// Ao logar:
try {
await buscarUsuario(123);
} catch (err) {
console.error(err); // exibe o erro atual
console.error(err.cause); // exibe o erro original do banco
}Exemplo prático: cadeia de erros em aplicação real
Em uma aplicação com camadas (controller, service, repository), o Error.cause permite rastrear o erro do banco até o response HTTP:
// Repository
async function findUser(id) {
try {
return await db.users.findById(id);
} catch (dbErr) {
throw new Error(`Repository: falha na query users.findById(${id})`, {
cause: dbErr
});
}
}
// Service
async function getUser(id) {
try {
return await findUser(id);
} catch (repoErr) {
throw new Error(`Service: falha ao carregar usuário ${id}`, {
cause: repoErr // preserva o erro do repository
});
}
}
// Controller
async function handleGetUser(req, res) {
try {
const user = await getUser(req.params.id);
res.json(user);
} catch (err) {
// err.cause = erro do service
// err.cause.cause = erro do repository
// err.cause.cause.cause = erro original do banco
logger.error("Erro ao buscar usuário", { error: err, cause: err.cause });
res.status(500).json({ error: err.message });
}
}Com essa cadeia, o log de produção mostra exatamente onde o problema ocorreu em cada camada, sem precisar de instrumentação adicional.
Comparação: Error.cause vs bibliotecas de erro
Bibliotecas como VError (Node.js) e verror ofereciam funcionalidade similar antes do Error.cause existir. Agora que a propriedade e nativa, essas bibliotecas perdem o principal argumento de adoção.
O Sentry e o Datadog já suportam Error.cause nativamente nos seus SDKs, exibindo a cadeia completa na interface de monitoramento de erros.
Para TypeScript, o tipo ErrorOptions inclui cause desde a versão 4.6, tornando o uso tipado direto sem nenhuma configuração extra.
Para classes de erro customizadas, passe as options para o super() do constructor: class DatabaseError extends Error { constructor(msg, options) { super(msg, options); } }. Isso preserva o cause em hierarquias de erros customizados.
Pontos positivos e limitações
O ponto positivo principal e a simplicidade: nenhuma biblioteca, nenhuma configuração, apenas uma propriedade nativa que todos os ambientes modernos suportam.
A limitação mais prática e a compatibilidade com código legado. Se você tem handlers que inspecionam erros serializando para JSON, a propriedade cause pode não aparecer por padrão porque o JSON.stringify não serializa propriedades de Error automaticamente.
Outra limitação e que a cadeia de cause pode se tornar profunda e verbosa em sistemas com muitas camadas. Nesse caso, uma estratégia de logging que extrai apenas as mensagens da cadeia pode ser mais legível do que logar o objeto completo.
Casos de uso reais
Integração com APIs externas: erros de timeout, rate limit e problemas de autenticação tem causas específicas que são perdidas sem Error.cause.
Queries de banco de dados: o erro original do driver de banco tem informações críticas (query, parâmetros, código de erro) que devem ser preservadas.
Parsers e validadores: erros de parsing tem posição no input, o tipo de erro esperado e outros detalhes que são perdidos ao criar um erro genérico.
Workflows asincronos: em chains de Promises com tratamento em cada etapa, Error.cause permite rastrear qual etapa falhou primeiro.
Dicas e boas práticas
Escreva uma função utilitária para extrair a cadeia completa de mensagens de erro: isso é útil para logs e para mensagens de erro ao usuário que precisam ser concisas mas informativas.
Não inclua dados sensíveis (tokens, senhas, PII) nas mensagens ou no cause dos erros que podem aparecer em logs. O Error.cause torna mais fácil propagar dados de erros pelo sistema, incluindo dados que não deveriam estar nos logs.
Crie um helper que serializa a cadeia de Error.cause para JSON de forma recursiva. O JSON.stringify padrão não inclui propriedades de Error (message, stack, cause), então logs de produção precisam de serialização customizada.
Vale a pena adotar Error.cause?
Sim, especialmente em código de produção com múltiplas camadas. A diferença entre debugar com e sem contexto de erro e enorme: reduce o tempo de investigação de bugs de horas para minutos.
Para projetos novos, adote como padrão imediato. Para projetos legados, comece pelos pontos de maior dor: integrações externas e queries de banco onde a causa do erro costuma ser perdida.
O próximo passo: encontre um try-catch no seu código que cria um erro novo sem passar o original como cause e adicione { cause: err }. E uma mudança de 15 caracteres que pode economizar horas na próxima investigação de bug em produção.