Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIntermediáriocódigo testado

Debugar JavaScript no DevTools: passo a passo

Breakpoint, breakpoint condicional, watch, call stack e step into/over/out no painel Sources — com o bug reproduzido e o console usado além do log.

Rodolfo Mori10 min de leitura

Depuração é o processo de reproduzir um defeito, interromper a execução e inspecionar o estado que levou até ele. Um console.log mostra apenas os valores que você escolheu antes; um breakpoint pausa o programa e deixa o escopo inteiro disponível para investigação.

Pense em congelar um filme no quadro anterior ao acidente. Scope mostra o que está em cena, Call Stack revela o caminho percorrido até aquele quadro e os comandos step into, over e out avançam a história em ritmos diferentes. No JavaScript, o breakpoint suspende a thread naquele ponto; ele não é uma imagem gravada, então você também pode avaliar expressões com o estado vivo.

No DevTools, abra Sources, clique no número da linha, recarregue a página e observe Scope. Um breakpoint condicional permite parar apenas no caso que reproduz o bug. A partir daí, seguimos um defeito de loja virtual do sintoma até a propriedade errada.

O log que não responde a pergunta

Este código calcula o total de um carrinho e aplica um cupom. O total sai NaN:

js
const cupons = { PRIMEIRACOMPRA: { desconto: 10 } };

function calcularTotal(itens, codigo) {
  let total = 0;
  for (const item of itens) {
    total += item.preco * item.quantidade;
  }
  const cupom = cupons[codigo];
  if (cupom) total -= total * cupom.percentual;
  return total;
}

const itens = [
  { nome: 'Teclado mecânico', preco: 289.9, quantidade: 1 },
  { nome: 'Mouse sem fio', preco: 149.9, quantidade: 2 },
];

console.log('itens:', itens.length);
console.log('total:', calcularTotal(itens, 'PRIMEIRACOMPRA'));
itens: 2 total: NaN

Os dois logs que você escreveu por instinto — quantos itens, qual o total — confirmam que o problema existe e não dizem nada sobre a causa. Para descobrir que o cupom tem desconto e o código lê percentual, você precisaria imprimir justamente o que não passou pela sua cabeça imprimir.

É esse o limite do log: ele mostra o que você já suspeitava. O breakpoint mostra o escopo inteiro, incluindo a variável que você nem lembrava que existia.

debugger: o breakpoint que mora no código

A palavra debugger é uma instrução da linguagem. Com o DevTools aberto, a execução para exatamente ali. Com o DevTools fechado, ela é ignorada:

js
function aplicarCupom(total, percentual) {
  debugger;
  return total - total * percentual;
}

console.log(aplicarCupom(602.64, 0.1));
542.376

A saída acima é do Node rodando sem depurador anexado: debugger não fez nada. Anexe um e o comportamento muda:

bash
node --inspect-brk=9229 calcular-total.mjs
Debugger listening on ws://127.0.0.1:9229/cd60ce37-6920-4b40-be7f-b3a39dff3a48 For help, see: https://nodejs.org/learn/getting-started/debugging

Com o processo esperando, abra chrome://inspect no Chrome e clique em inspect. O painel Sources abre com o mesmo arquivo, e o --inspect-brk garante que ele já esteja parado na primeira linha — sem isso, um script curto termina antes de você conseguir clicar.

O painel Sources, na ordem em que se usa

No navegador você não precisa editar o código para colocar um breakpoint. O caminho é sempre o mesmo:

  1. F12 (ou Cmd+Option+I no macOS) abre o DevTools. Aba Sources.
  2. Cmd+P / Ctrl+P abre a busca de arquivos. Digite o nome do arquivo — funciona igual ao “ir para arquivo” do VS Code.
  3. Clique no número da linha. Ele fica azul: é um breakpoint.
  4. Recarregue a página ou dispare a ação. A execução congela na linha, com ela destacada.
  5. O painel da direita se enche: Watch, Breakpoints, Scope, Call Stack.

Enquanto o programa está pausado, o console continua vivo — e vive dentro daquele escopo. Digitar cupom no console te dá o valor real da variável naquele instante, sem console.log nenhum.

Breakpoint condicional: parar só no caso que importa

Um breakpoint dentro de um laço que roda 300 vezes é inútil se você precisa da volta 287. Considere este código, que funciona para um cupom e quebra no outro:

js
const cupons = {
  PRIMEIRACOMPRA: { percentual: 0.1, regras: { minimo: 99 } },
  BLACKFRIDAY: { percentual: 0.3 },
};

function aplicarCupom(total, codigo) {
  const cupom = cupons[codigo];
  if (total < cupom.regras.minimo) return total;
  return total - total * cupom.percentual;
}

for (const codigo of ['PRIMEIRACOMPRA', 'BLACKFRIDAY']) {
  console.log(codigo, '->', aplicarCupom(589.7, codigo).toFixed(2));
}
PRIMEIRACOMPRA -> 530.73 file:///private/tmp/loja/d10.mjs:8 if (total < cupom.regras.minimo) return total; ^ TypeError: Cannot read properties of undefined (reading 'minimo') at aplicarCupom (file:///private/tmp/loja/d10.mjs:8:28) at file:///private/tmp/loja/d10.mjs:13:29 at ModuleJob.run (node:internal/modules/esm/module_job:439:25) at async node:internal/modules/esm/loader:633:26 at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5) Node.js v24.16.0

A primeira iteração passou. A segunda quebrou. Um breakpoint comum na linha 8 para nas duas — e na primeira vez você olha um estado perfeitamente saudável.

Clique com o botão direito no número da linha, escolha Add conditional breakpoint e escreva a condição:

js
codigo === 'BLACKFRIDAY'

Agora a execução ignora a primeira volta e para só na segunda, com cupom já carregado no Scope — onde fica evidente que o objeto tem percentual e não tem regras.

O mesmo menu oferece mais duas opções que valem ouro:

opção o que faz quando usar
Conditional breakpoint para só quando a expressão é verdadeira laço longo, lista com um item problemático
Logpoint imprime uma expressão e não para substitui o console.log sem editar o arquivo
Never pause here desliga a pausa naquela linha biblioteca de terceiro que dispara o tempo todo

O logpoint é o mais subestimado: ele te dá o log sem alterar o código, sem rebuild e sem risco de esquecer o console.log no commit.

Scope, Watch e o valor no momento exato

Com a execução parada, o painel Scope mostra três camadas empilhadas:

  • Local — parâmetros e variáveis da função atual. É aqui que o cupom sem regras aparece.
  • Closure — o que a função enxerga do escopo onde nasceu.
  • Globalwindow no navegador, globalThis no Node.

O painel Watch é para expressões, não só variáveis. Clique no + e adicione, por exemplo, cupom?.regras?.minimo ou itens.reduce((s, i) => s + i.preco, 0). A expressão é reavaliada a cada pausa, o que transforma o Watch num painel de instrumentos: você acompanha o valor mudando enquanto avança linha a linha.

Diferença prática que economiza tempo: console.log de um objeto no Node serializa na hora e corta a profundidade; o Scope não corta nada.

js
const pedido = {
  id: 4821,
  cliente: { nome: 'Ana', endereco: { cidade: 'São Paulo', geo: { lat: -23.55, lng: -46.63 } } },
  itens: [{ nome: 'Teclado mecânico', preco: 289.9 }],
};

console.log(pedido);
console.log('---');
console.dir(pedido, { depth: null });
{ id: 4821, cliente: { nome: 'Ana', endereco: { cidade: 'São Paulo', geo: [Object] } }, itens: [ { nome: 'Teclado mecânico', preco: 289.9 } ] } --- { id: 4821, cliente: { nome: 'Ana', endereco: { cidade: 'São Paulo', geo: { lat: -23.55, lng: -46.63 } } }, itens: [ { nome: 'Teclado mecânico', preco: 289.9 } ] }

Aquele [Object] no primeiro log é exatamente o dado que você precisava ver. console.dir(obj, { depth: null }) resolve no Node; no breakpoint, o problema nem existe.

Call Stack: subir até quem causou

O painel Call Stack lista as funções empilhadas, da atual para a mais antiga. Clicar em qualquer quadro muda o Scope para o daquele quadro — você literalmente volta no tempo da chamada e vê com que argumentos ela foi feita.

É o mesmo rastro que aparece nas stack traces de erro, detalhado em tipos de erro em JavaScript. A diferença é que aqui ele está vivo: dá para inspecionar as variáveis de cada nível, não só ler o nome.

No console, console.trace imprime esse mesmo rastro sem parar nada — útil para descobrir quem chamou uma função que não deveria ter sido chamada:

js
function aplicarFrete(pedido) {
  if (pedido.total === undefined) {
    console.trace('pedido sem total chegou aqui');
    return 0;
  }
  return pedido.total >= 199 ? 0 : 24.9;
}

function finalizarCompra(pedido) {
  return aplicarFrete(pedido);
}

console.log('frete:', finalizarCompra({ cliente: 'Ana' }));
Trace: pedido sem total chegou aqui at aplicarFrete (file:///private/tmp/loja/d09.mjs:3:13) at finalizarCompra (file:///private/tmp/loja/d09.mjs:10:10) at file:///private/tmp/loja/d09.mjs:13:23 at ModuleJob.run (node:internal/modules/esm/module_job:439:25) at async node:internal/modules/esm/loader:633:26 at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5) frete: 0

O frete saiu zero sem erro nenhum. O trace mostra que a origem é a linha 13, onde alguém montou um pedido sem total.

Step into, over e out

Com a execução parada, quatro botões controlam o avanço. Os atalhos são os mesmos no Chrome, no Firefox e no depurador do VS Code:

botão atalho o que faz
Resume F8 solta o programa até o próximo breakpoint
Step over F10 executa a linha inteira, inclusive chamadas, e para na próxima
Step into F11 entra na função chamada nesta linha
Step out Shift+F11 termina a função atual e para em quem a chamou

A regra prática: step over por padrão, step into quando você desconfia da função chamada. Entrar em toda função é como ler um livro letra por letra — você chega em node_modules no terceiro F11 e perde o fio.

Quando isso acontece, marque o arquivo de terceiro como ignorado: botão direito no arquivo em Sources → Add script to ignore list. O depurador passa a atravessá-lo sem parar, e o Call Stack para de mostrar quadros de biblioteca.

Breakpoint em evento e em requisição

Nem todo bug tem uma linha óbvia para marcar. Às vezes o que você sabe é “algo acontece quando eu clico” ou “algo acontece depois que a API responde”. A coluna direita do Sources tem duas seções para isso.

Event Listener Breakpoints — expanda Mouse → click e o depurador para na primeira linha do listener que responder ao próximo clique, seja ele seu ou de uma biblioteca. É a forma mais rápida de descobrir que código está por trás de um botão:

js
document.querySelector('#finalizar-compra').addEventListener('click', (evento) => {
  evento.preventDefault();
  enviarPedido(montarPedido());
});

XHR/fetch Breakpoints — clique no + e digite um pedaço da URL, por exemplo /api/frete. Toda requisição cuja URL contiver esse texto pausa antes de sair, com o Scope no ponto da chamada. Serve para pegar o corpo errado sendo montado, antes de ir para a rede:

js
async function consultarFrete(cep, peso) {
  const resposta = await fetch(`/api/frete?cep=${cep}&peso=${peso}`);
  if (!resposta.ok) throw new Error(`frete indisponível (${resposta.status})`);
  return resposta.json();
}

Há ainda DOM Breakpoints: botão direito num elemento na aba Elements → Break on → attribute modifications. Quando você não faz ideia de qual código está mudando a classe de um elemento, esse é o atalho.

Depurar código assíncrono

Uma pausa dentro de um .then ou depois de um await mostraria, por padrão, uma call stack quase vazia — a pilha original já foi desmontada quando o callback roda. O DevTools resolve isso com Async stack traces, ligadas por padrão: os quadros anteriores aparecem separados por uma linha async, e você consegue clicar neles.

js
async function finalizarCompra(carrinho) {
  const frete = await consultarFrete(carrinho.cep, carrinho.peso);
  const total = carrinho.subtotal + frete.valor;
  return confirmarPagamento(total);
}

Marque a linha do const total e observe o Call Stack: além de finalizarCompra, aparecem os quadros de quem iniciou a operação. Sem isso, você veria só o quadro do microtask.

Duas dicas específicas de assíncrono:

  • Marque a linha do await, não a de dentro da função esperada. O breakpoint no await te deixa inspecionar o valor depois que ele chegou.
  • Na aba Network, a coluna Initiator dá o mesmo rastro para requisições — clicar nela abre o Sources na linha que disparou o fetch.

Quando o erro assíncrono escapa de um try/catch, a causa costuma ser a que está explicada em try catch em JavaScript: o bloco já tinha terminado quando o callback rodou.

O console além do log

Nem tudo pede breakpoint. Além do log, o console tem cinco métodos que resolvem casos inteiros sozinhos — table, count, group, assert e time:

js
const itens = [
  { nome: 'Teclado mecânico', preco: 289.9, quantidade: 1 },
  { nome: 'Mouse sem fio', preco: 149.9, quantidade: 2 },
  { nome: 'Mousepad XL', preco: 79.9, quantidade: 1 },
];

console.table(itens, ['nome', 'preco', 'quantidade']);
┌─────────┬────────────────────┬───────┬────────────┐ │ (index) │ nome │ preco │ quantidade │ ├─────────┼────────────────────┼───────┼────────────┤ │ 0 │ 'Teclado mecânico' │ 289.9 │ 1 │ │ 1 │ 'Mouse sem fio' │ 149.9 │ 2 │ │ 2 │ 'Mousepad XL' │ 79.9 │ 1 │ └─────────┴────────────────────┴───────┴────────────┘

console.count conta quantas vezes um trecho rodou — matador para descobrir render duplicado ou listener registrado duas vezes. console.group aninha a saída. console.assert só fala quando a condição é falsa:

js
const pedidos = [
  { id: 4821, uf: 'SP', total: 289.9 },
  { id: 4822, uf: 'RJ', total: 1199 },
  { id: 4823, uf: 'SP', total: 79.9 },
];

for (const pedido of pedidos) {
  console.count(`frete calculado para ${pedido.uf}`);
}

console.group('pedido 4822');
console.log('uf   :', pedidos[1].uf);
console.log('total:', pedidos[1].total);
console.groupEnd();

console.assert(pedidos[2].total >= 199, 'pedido %d abaixo do frete grátis', pedidos[2].id);
frete calculado para SP: 1 frete calculado para RJ: 1 frete calculado para SP: 2 pedido 4822 uf : RJ total: 1199 Assertion failed: pedido 4823 abaixo do frete grátis

E console.time mede, o que evita discussão de achismo sobre desempenho:

js
const itens = Array.from({ length: 200000 }, (_, i) => ({ preco: (i % 100) + 0.9 }));

console.time('reduce');
const totalReduce = itens.reduce((soma, item) => soma + item.preco, 0);
console.timeEnd('reduce');

console.time('for');
let totalFor = 0;
for (let i = 0; i < itens.length; i++) totalFor += itens[i].preco;
console.timeEnd('for');

console.log(totalReduce.toFixed(2), totalFor.toFixed(2));
reduce: 1.626ms for: 0.677ms 10080000.00 10080000.00

Duzentos mil itens e a diferença é de um milissegundo. Guarde o número antes de reescrever um reduce legível como for por “performance”.

O método, em quatro passos

Depois de acompanhar centenas de sessões de depuração de aluno, o que separa quem resolve em cinco minutos de quem passa a tarde é o método, não a ferramenta:

  1. Reproduza com o menor caso possível. Dois cupons, dois itens. Bug que só acontece com o carrinho inteiro é bug que você não entendeu ainda.
  2. Formule uma hipótese antes de marcar a linha. “Acho que cupom chega sem regras”. Sem hipótese, o breakpoint vira turismo.
  3. Pare no ponto onde o valor ainda deveria estar certo, não onde ele já está errado. O erro estoura no consumidor; a causa está no produtor.
  4. Confira no Scope, não na memória. Se você “tem certeza” de que a variável vale X, é exatamente ela que precisa ser olhada.

Erro reproduzido e escopo inspecionado é o suficiente para 90% dos bugs. Para os outros 10%, o próximo passo da trilha de JavaScript ajuda: quando o código está espalhado em vários arquivos, saber como os módulos são carregados explica por que o breakpoint às vezes está num arquivo que o navegador nem baixou. E se o erro que você está caçando é o mais comum de todos, ele tem página própria: TypeError: Cannot read properties of undefined.

Repita o bug do cupom sem adicionar novos logs. Marque uma pausa antes do cálculo do desconto, adicione cupom e total ao Watch e avance uma linha por vez. A prática termina quando você consegue apontar a primeira linha em que o valor deixa de corresponder à hipótese e explicar, pela Call Stack, quem chamou aquela função.

  • debug
  • devtools
  • breakpoint
  • console
  • call stack

Perguntas frequentes

Breakpoint substitui o console.log?
Substitui na hora de investigar, não na hora de monitorar. Para descobrir por que um valor está errado, o breakpoint mostra o escopo inteiro sem você adivinhar o que imprimir. Para acompanhar um fluxo que roda cem vezes por segundo, ou algo em produção, o log continua sendo a ferramenta.
Deixei um debugger no código e foi para produção. É grave?
Sem DevTools aberto, a instrução é ignorada e nada acontece — o programa roda normalmente. O problema é quem abrir o console: a página congela ali. Trate como um console.log esquecido e barre no lint com a regra no-debugger.
Por que o breakpoint não para na linha que eu marquei?
Quase sempre é source map desatualizado: você marca no arquivo original e o navegador executa outro build. Recarregue com cache desabilitado, ou marque no arquivo que aparece em Sources depois do reload. Se a linha nem tiver o marcador azul, ela não faz parte do código executado.
Dá para depurar código Node no DevTools do Chrome?
Dá. Rode node --inspect-brk arquivo.js e abra chrome://inspect. O Chrome lista o processo, e o painel Sources funciona igual ao de uma página — mesmos breakpoints, mesmo watch, mesma call stack.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. Chrome DevTools — Debug JavaScript — developer.chrome.com
  2. MDN — debugger — developer.mozilla.org
  3. Node.js — Debugging Guide — nodejs.org

Continue por aqui