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.

💡
Dica

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.cause em 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.
⚠️
Atenção

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.

🚀
Pro tip

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

💡
Dica

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.

🔴
Cuidado

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.

🚀
Pro tip

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.