12k
All articles

Como Ler um Stack Trace de JavaScript

Leia stack traces de JavaScript: identifique o primeiro frame útil, entenda lacunas async, código minificado, source maps e Error.cause.

OpenReplay Team
OpenReplay Team
Como Ler um Stack Trace de JavaScript

Um stack trace de JavaScript é lido de trás para frente no tempo: o frame do topo é a chamada que lançou o erro, e cada frame abaixo dele é a chamada que levou até ali.

O movimento habitual é passar os olhos pela primeira linha, colá-la em uma caixa de busca e torcer. Isso funciona até que o frame do topo pertença ao React, ou ao JSON.parse, ou a um bundle onde toda função se chama o.

Este artigo percorre um trace do início ao fim, acrescentando em cada seção uma forma de lê-lo: qual frame realmente abrir, o que os frames async estão dizendo, como é um trace minificado e por que a pilha por trás de Error.cause nunca aparece na pilha que você imprimiu.

Principais Conclusões

  • O frame do topo é onde o erro foi lançado, não onde o bug foi introduzido; o primeiro frame apontando para um arquivo que você escreveu é onde a investigação começa.
  • Frames assíncronos marcados com async são reconstruídos pelo V8 a partir dos pontos em que cada await pausou e retomou, então await, Promise.all() e Promise.any() são costurados, enquanto uma cadeia simples de .then() deixa uma lacuna.
  • O V8 mantém apenas 10 frames por padrão, um número que você pode alterar através do não padronizado Error.stackTraceLimit.
  • new Error(message, { cause }) preserva o erro original, mas o engine não mescla as duas pilhas: err.stack mostra apenas o wrapper.

Como É um Stack Trace de JavaScript?

Um stack trace de JavaScript é um nome de erro e uma mensagem na primeira linha, seguidos de um frame por chamada, do mais recente para o mais antigo. Aqui está o trace ao qual este artigo volta constantemente: um arquivo de carrinho é lido do disco, parseado e entregue ao restante da aplicação.

SyntaxError: Unexpected token 'b', "{ bad json" is not valid JSON
    at JSON.parse (<anonymous>)
    at parseCart (/app/src/cart.js:5:15)
    at loadCart (/app/src/cart.js:10:20)
    at main (/app/src/main.js:6:16)
    at Object.<anonymous> (/app/src/main.js:12:1)

Leia os frames nessa ordem e a cadeia fica evidente: JSON.parse lançou o erro, parseCart o chamou, loadCart chamou parseCart, e assim por diante. O frame de baixo é onde aquela cadeia específica de chamadas começou, o que nem sempre é o ponto de entrada do programa: código alcançado através de um manipulador de clique, um callback de timer ou uma promise resolvida ganha uma cadeia nova que começa no callback.

Um único frame te dá quatro coisas: o nome da função, o arquivo, a linha e a coluna. Em código minificado, só a coluna vale alguma coisa sem um source map. Uma ressalva que vale conhecer desde cedo: Error.prototype.stack não é padronizado. Todo engine o entrega, cada um imprime uma string ligeiramente diferente, e o trabalho no TC39 para fixar o formato está inacabado. Os exemplos aqui têm o formato do V8, o que cobre Chrome, Edge e Node.

O Frame do Topo Geralmente Não É o Seu Código

O frame do topo de um stack trace geralmente não é o seu código. É a biblioteca, o framework ou o recurso nativo que percebeu o valor inválido, o que significa que ele te diz o que quebrou, não por quê. No trace acima, at JSON.parse (<anonymous>) é o parser nativo relatando que uma string que lhe foi entregue não é JSON válido. Não há nada a corrigir ali.

O frame que você quer é o primeiro que aponta para um arquivo que você escreveu. Esse é parseCart em /app/src/cart.js:5:15. Abra essa linha e você encontra a chamada a JSON.parse, o que diz que a string inválida chegou como argumento. Portanto o valor veio do frame abaixo: loadCart, na linha 10, que leu o arquivo. Esse é o verdadeiro ponto de partida, e a pergunta que ele responde é de onde vieram os conteúdos do arquivo e por que nada os validou.

Essa ordem de leitura se generaliza. Percorra para baixo passando por caminhos em node_modules, frames <anonymous> e native até chegar ao seu próprio arquivo, e então avance para baixo pelos frames que forneceram o valor.

Um trace registra o caminho que o programa percorreu até a falha, nunca o caminho que o usuário percorreu, e é por isso que dois relatos com frames idênticos podem ser uma correção de cinco minutos e um fantasma irreproduzível. O session replay fecha essa metade da lacuna: você lê os frames para saber onde quebrou e assiste à sessão para entender como a aplicação chegou a um estado em que aquilo poderia quebrar.

Frames Assíncronos e a Fronteira do Await

Marque loadCart como async e o trace atravessa o await, com os frames reconstruídos prefixados por async:

SyntaxError: Unexpected token 'b', "{ bad json" is not valid JSON
    at JSON.parse (<anonymous>)
    at parseCart (/app/src/cart.js:5:15)
    at async loadCart (/app/src/cart.js:10:20)
    at async main (/app/src/main.js:6:16)

Esses frames prefixados com async não são capturados da mesma forma que os frames síncronos. O V8 os reconstrói a partir dos pontos de await, e consegue fazer isso sem custo porque um await retoma exatamente no ponto em que pausou.

A reconstrução tem limites, e esses limites são onde o trace se afina. A costura alcança pontos de await, Promise.all() e Promise.any(), e nada mais, então uma promise retornada sem ser aguardada, ou uma cadeia de .then(), deixa um buraco exatamente onde estava o contexto chamador. Se os frames param abruptamente em uma fronteira assíncrona, procure por um await faltando na camada acima. Não há flag para ativar: --async-stack-traces está ativo por padrão desde o V8 v7.3, então frames assíncronos aparecem no Chrome, em todas as versões mantidas do Node e em outros runtimes V8 atuais.

A outra razão para frames desaparecerem é o limite. O V8 mantém 10 frames e descarta o resto, e Error.stackTraceLimit é o botão para esse número: um novo valor se aplica a erros criados depois que você o define, e qualquer coisa que não seja um número, ou que seja menor que zero, te deixa sem frame algum.

if (process.env.NODE_ENV !== 'production') {
  Error.stackTraceLimit = Infinity;
}

Restrinja isso ao ambiente de desenvolvimento. Traces profundos custam memória, enterram os frames interessantes em ruído e empurram caminhos de arquivo e nomes de função para logs que podem ser enviados a outros lugares. A propriedade não é padronizada: ela surgiu no V8, e o JavaScriptCore a copiou por compatibilidade, então defini-la não quebra nada em lugar algum, mas o valor padrão e os detalhes finos dependem do engine.

Como Ler um Trace Minificado?

Contra um bundle de produção, a mesma falha produz frames assim. Trate o formato como ilustrativo de saída empacotada, e não como o formato exato de uma ferramenta específica:

SyntaxError: Unexpected token 'b', "{ bad json" is not valid JSON
    at JSON.parse (<anonymous>)
    at o (/assets/index-4f1c8a2b.js:1:20874)
    at async s (/assets/index-4f1c8a2b.js:1:21036)

A assinatura é inconfundível: nomes de função de uma única letra, um nome de arquivo, linha 1 e uma coluna de cinco ou seis dígitos. Linha 1 e uma coluna enorme significam que todo o grafo de módulos está em uma única linha, então a coluna é a única coordenada que carrega informação. parseCart e loadCart ainda existem naquele deslocamento de coluna, mas nada na string vai te dizer seus nomes.

Recuperá-los exige um source map que a ferramenta consiga alcançar, gerado em tempo de build e enviado para algum lugar contra o qual o trace possa ser resolvido. Nosso guia sobre como funcionam os source maps cobre o formato e a configuração de build.

Error.cause Não Mescla Pilhas

Envolver um erro com new Error(message, { cause: originalError }) preserva o objeto de erro original junto com seu tipo e sua própria pilha, mas o engine não mescla as duas pilhas. err.stack mostra apenas o wrapper, e o original é acessível exclusivamente através de err.cause.stack.

export async function loadCart(path) {
  const raw = await readFile(path, 'utf8');
  try {
    return parseCart(raw);
  } catch (err) {
    throw new Error(`Cart file ${path} is not valid JSON`, { cause: err });
  }
}

Quem chama agora recebe uma mensagem nomeando o arquivo, e err.cause instanceof SyntaxError continua valendo. O que não recebem é o frame de JSON.parse: a pilha do wrapper começa no throw dentro de loadCart. Error.cause chegou no ES2022 e funciona nos navegadores atuais e em todas as versões mantidas do Node, retroagindo até o Node 16.9.0. Ela é definida como uma propriedade própria não enumerável, portanto fica fora de Object.keys(), for...in e de um JSON.stringify() ingênuo do erro.

O quanto de uma cadeia um console imprime varia por runtime e por console, então o movimento portável é percorrê-la você mesmo:

function printChain(error) {
  let current = error;
  while (current instanceof Error) {
    console.error(current.stack);
    current = current.cause;
  }
}

Isso imprime os frames do wrapper e, em seguida, os do parser, na ordem em que foram lançados.

Dois Hábitos Que Destroem o Rastro

Capturar um erro e lançar um novo sem passar uma causa apaga o trace original do programa. Os frames que teriam dito onde o valor inválido entrou não existem mais em lugar nenhum, e nenhuma quantidade de busca em logs os traz de volta:

catch (err) {
  throw new Error('Could not load cart');   // JSON.parse frame is gone
}

Engolir o erro em uma linha de log causa o mesmo dano, de forma mais silenciosa:

catch (err) {
  console.log('cart load failed');          // message, no stack, no type
  return [];
}

Ambos estão a uma palavra-chave de distância de ficarem corretos. Passe { cause: err } quando relançar, e registre o próprio err em vez de uma frase sobre ele.

Da próxima vez que um trace cair na sua frente, não comece pela linha um. Role até o primeiro frame com o nome de um arquivo seu, abra aquela linha e pergunte qual valor lhe foi entregue e por quem. Se os frames param em uma fronteira async ou em uma função de uma única letra, você está olhando para uma lacuna de costura ou para um source map faltando, não para a história completa.

Perguntas Frequentes

Por que meu manipulador de erros só reporta 'Script error.' sem nenhuma pilha?

Os navegadores mascaram os detalhes de exceções lançadas por scripts de origem cruzada, então window.onerror recebe o texto genérico 'Script error.' sem URL, número de linha ou pilha úteis. Para obter a mensagem e os frames reais, carregue o script com o atributo crossorigin definido como anonymous e certifique-se de que o servidor que o hospeda retorne um cabeçalho Access-Control-Allow-Origin cobrindo a sua origem. A maioria das CDNs públicas já envia esse cabeçalho.

Error.captureStackTrace funciona fora do Chrome e do Node?

Não é mais exclusivo do V8. Error.captureStackTrace nasceu no V8 como parte de sua API não padronizada de stack trace, e os outros engines o seguiram desde então: o JavaScriptCore o disponibilizou no Safari 17.2, lançado em 11 de dezembro de 2023, e o SpiderMonkey no Firefox 138, lançado em 29 de abril de 2025. Chamá-lo escreve uma string de pilha em qualquer objeto que você passar. Como ainda não é padronizado, proteja a chamada com uma verificação typeof Error.captureStackTrace antes de usá-lo em código de biblioteca compartilhada.

Por que os stack traces parecem diferentes no Firefox e no Chrome?

Error.prototype.stack fica fora de qualquer especificação, então cada engine o imprime do jeito que prefere e o conteúdo varia. O V8 escreve cada frame em uma linha começando com 'at', enquanto o Firefox usa um formato functionName@file:line:column sem esse prefixo. Trate a string da pilha como saída legível por humanos, não como uma API parseável, e nunca construa agrupamento de erros apenas sobre uma regex artesanal.

Posso capturar um stack trace sem lançar um erro?

Sim. Na maioria dos engines atuais a pilha é preenchida quando você constrói o Error, não quando o lança, então const { stack } = new Error() te entrega a pilha de chamadas na hora, sem throw e sem catch. O frame do topo é a linha que criou o erro, e o limite usual de frames continua valendo: o V8 mantém apenas 10 frames a menos que Error.stackTraceLimit seja aumentado.

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.