JavaScript assíncrono: event loop e call stack
Uma thread, uma pilha e duas filas: por que um laço longo congela a página e em que ordem síncrono, microtask e macrotask realmente saem no console.
JavaScript executa uma coisa de cada vez. Existe uma única thread rodando o seu código, e uma única pilha de chamadas nela. Tudo que parece simultâneo — requisição, temporizador, clique — é espera delegada para fora do JavaScript e devolvida depois, em fila.
A resposta curta, para você já conseguir prever a saída de qualquer código: o
motor roda todo o código síncrono até a pilha esvaziar; depois esvazia a fila de
microtasks (o que vem de Promise); só então pega uma macrotask
(setTimeout, evento, I/O) e recomeça o ciclo.
Todos os exemplos abaixo rodaram no Node 24.16.0, no mesmo domínio das lições anteriores da trilha de JavaScript: uma loja com carrinho, frete e estoque.
O nome técnico do modelo é run-to-completion: cada tarefa JavaScript termina antes de outra começar na mesma thread. Em palavras simples, a espera pode acontecer fora, mas nenhum callback interrompe uma função no meio.
A cozinha com uma bancada e duas filas de retirada
Imagine uma cozinha com uma única bancada de finalização. Pratos podem assar e esfriar fora dela, mas só um pedido é montado por vez. Quando a bancada libera, a equipe atende primeiro toda a fila curta de ajustes urgentes e depois pega um pedido da fila comum. A bancada representa a pilha; microtasks e macrotasks são as filas com regras de prioridade diferentes.
Antes de cada exemplo, separe os logs em três colunas: síncrono, microtask e macrotask. Numere a ordem dentro de cada coluna e só depois junte as listas. Execute e compare. Esse procedimento permite prever a saída sem usar “assíncrono é aleatório” como explicação.
Cada chamada de função empilha um quadro; cada return desempilha. Quando algo
estoura, o Node imprime a pilha inteira de baixo para cima — e é a fotografia
mais honesta do que estava rodando naquele instante.
function precoComFrete(preco, peso) {
return preco + calcularFrete(peso);
}
function calcularFrete(peso) {
return 12.5 + adicionalPorPeso(peso);
}
function adicionalPorPeso(peso) {
return peso.kg * 3;
}
console.log('inicio');
console.log(precoComFrete(289.9));
console.log('fim');Três funções empilhadas, na ordem exata das chamadas, mais a linha do módulo que
começou tudo — e o fim que nunca foi impresso. A pilha é síncrona: nada mais acontece enquanto ela não esvazia. O
mecanismo desse erro específico está em
TypeError: Cannot read properties of undefined.
Enquanto a pilha não esvazia, nada anda
Este é o experimento que convence. Agendo um timer para “agora” (0ms) e, logo
em seguida, faço um laço de dois bilhões de voltas:
const inicio = Date.now();
setTimeout(() => {
console.log('timer pedido para 0ms rodou depois de', Date.now() - inicio, 'ms');
}, 0);
let soma = 0;
for (let i = 0; i < 2_000_000_000; i++) soma += i;
console.log('laco terminou em', Date.now() - inicio, 'ms');O timer estava pronto desde o primeiro milissegundo e ficou 1,3 segundo esperando a pilha desocupar. Num navegador, esse mesmo laço é 1,3 segundo de página congelada: sem repintar, sem responder a clique, sem rolar.
A ordem real: síncrono, microtask, macrotask
Este trecho está escrito de propósito fora de ordem. Numerei as linhas pela ordem em que elas realmente saem:
console.log('1 — sincrono: abriu o carrinho');
setTimeout(() => console.log('4 — macrotask: setTimeout 0'), 0);
Promise.resolve().then(() => console.log('3 — microtask: Promise.then'));
console.log('2 — sincrono: fechou o carrinho');O setTimeout foi agendado antes do Promise.then e mesmo assim saiu por
último. Não é sorte: são filas diferentes, com prioridades diferentes.
Duas filas, e uma delas nunca fica pela metade
A regra que resolve os casos difíceis: a fila de microtasks é drenada até o fim antes da próxima macrotask — inclusive as microtasks criadas por outra microtask no meio da drenagem.
setTimeout(() => console.log('macrotask A'), 0);
setTimeout(() => {
console.log('macrotask B');
Promise.resolve().then(() => console.log(' microtask criada dentro de B'));
}, 0);
Promise.resolve().then(() => {
console.log('microtask 1');
Promise.resolve().then(() => console.log('microtask 2 (criada dentro da 1)'));
});
queueMicrotask(() => console.log('microtask 3'));
console.log('sincrono');Repare na microtask 2: ela nasceu durante a drenagem e ainda assim entrou
antes da macrotask A. E repare na última linha: depois de cada macrotask, o
event loop volta a esvaziar as microtasks antes de seguir.
As filas extras do Node
No navegador existem essas duas filas. No Node existem outras, e a ordem entre
elas muda conforme o tipo do arquivo. Em um módulo ES (.mjs):
setTimeout(() => console.log('timers: setTimeout'), 0);
setImmediate(() => console.log('check: setImmediate'));
Promise.resolve().then(() => console.log('microtask: Promise'));
process.nextTick(() => console.log('nextTick'));
console.log('sincrono');O mesmo arquivo salvo como .cjs, sem trocar uma linha, inverte as duas do
meio:
setTimeout(() => console.log('timers: setTimeout'), 0);
setImmediate(() => console.log('check: setImmediate'));
Promise.resolve().then(() => console.log('microtask: Promise'));
process.nextTick(() => console.log('nextTick'));
console.log('sincrono');O process.nextTick tem prioridade sobre as microtasks de Promise — mas o corpo
de um módulo ES já é avaliado dentro de um contexto de microtask, e por isso o
then chega primeiro. É um detalhe que só aparece quando você mede. A lição
prática é a mesma dos dois lados: não escreva código que dependa da ordem
entre nextTick e Promise.
O erro que todo mundo comete uma vez
Você chama a API, guarda o resultado numa variável e usa logo abaixo. O código parece certo:
function buscarProdutos() {
let produtos;
fetch('http://localhost:4599/produtos')
.then((resposta) => resposta.json())
.then((dados) => {
produtos = dados;
});
return produtos;
}
const lista = buscarProdutos();
console.log(lista);
console.log(lista.length);O return produtos roda no mesmo tique do relógio em que a função foi chamada.
Nesse instante o fetch mal saiu pela placa de rede: o then só vira microtask
lá na frente, quando a resposta chega. A variável ainda vale undefined, e o
.length estoura.
A correção não é “esperar um pouco” com setTimeout — é declarar a espera:
async function buscarProdutos() {
const resposta = await fetch('http://localhost:4599/produtos');
return resposta.json();
}
const lista = await buscarProdutos();
console.log(lista.length, 'produtos');
console.log(lista[0].nome);Note o detalhe que fecha o raciocínio: buscarProdutos continua devolvendo na
hora. O que ela devolve agora é uma Promise, e é o await de quem chamou que
segura o valor até chegar. Esse contrato é o assunto de
Promise em JavaScript e de
async e await.
Como ler código assíncrono sem se enganar
Três perguntas resolvem quase todo caso:
- Essa linha roda agora ou depois?
fetch,setTimeout,addEventListenerereadFileregistram e voltam imediatamente. O corpo do callback é “depois”. - Quem está segurando a pilha? Se a interface travou, procure o laço, o
JSON.parsede 30 MB ou osortde cem mil itens — não a rede. - De qual fila esse callback vem? Se veio de Promise, ele fura a fila do
setTimeout. Se veio de timer, ele espera a vez.
E uma regra de escrita: uma função que faz trabalho assíncrono devolve Promise,
sempre. Função que “às vezes devolve o valor, às vezes devolve undefined”
transforma o bug em loteria — quem chama não tem como saber o que esperar. Se
esse conceito de valor devolvido ainda estiver cru, vale revisar
funções em JavaScript.
A próxima lição desmonta a peça mais simples do lado assíncrono, o setTimeout e o setInterval — onde o atraso que você pede quase nunca é o atraso que você recebe.
Perguntas frequentes
JavaScript é single-thread mesmo com tanta coisa acontecendo junto?
Qual a diferença prática entre microtask e macrotask?
Por que meu console.log imprime undefined logo depois do fetch?
Um laço grande realmente trava a página inteira?
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
- MDN — Modelo de concorrência e event loop — developer.mozilla.org
- Node.js — The Node.js Event Loop — nodejs.org


