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.
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:
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'));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:
function aplicarCupom(total, percentual) {
debugger;
return total - total * percentual;
}
console.log(aplicarCupom(602.64, 0.1));A saída acima é do Node rodando sem depurador anexado: debugger não fez nada.
Anexe um e o comportamento muda:
node --inspect-brk=9229 calcular-total.mjsCom 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:
F12(ouCmd+Option+Ino macOS) abre o DevTools. Aba Sources.Cmd+P/Ctrl+Pabre a busca de arquivos. Digite o nome do arquivo — funciona igual ao “ir para arquivo” do VS Code.- Clique no número da linha. Ele fica azul: é um breakpoint.
- Recarregue a página ou dispare a ação. A execução congela na linha, com ela destacada.
- 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:
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));
}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:
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
cupomsemregrasaparece. - Closure — o que a função enxerga do escopo onde nasceu.
- Global —
windowno navegador,globalThisno 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.
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 });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:
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' }));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:
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:
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.
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 noawaitte 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:
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']);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:
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);E console.time mede, o que evita discussão de achismo sobre desempenho:
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));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:
- 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.
- Formule uma hipótese antes de marcar a linha. “Acho que
cupomchega semregras”. Sem hipótese, o breakpoint vira turismo. - 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.
- 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.
Perguntas frequentes
Breakpoint substitui o console.log?
Deixei um debugger no código e foi para produção. É grave?
Por que o breakpoint não para na linha que eu marquei?
Dá para depurar código Node no DevTools do Chrome?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
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
- Chrome DevTools — Debug JavaScript — developer.chrome.com
- MDN — debugger — developer.mozilla.org
- Node.js — Debugging Guide — nodejs.org


