Este é um dos artigos mais importantes de toda a série. Não porque seja o mais difícil — mas porque ele explica como o JavaScript realmente funciona. Muita gente usa Promises e async/await sem entender por quê existem. Esse artigo resolve isso.
Quando você entender o Event Loop, comportamentos que antes pareciam mágica ou bug vão fazer sentido perfeito. Vamos com calma.
O problema: JavaScript é single-threaded
JavaScript executa em uma única thread — isso significa que ele só faz uma coisa de cada vez. Não há paralelismo real. Enquanto uma linha de código executa, nenhuma outra pode executar ao mesmo tempo.
Parece uma limitação grave. E seria — se não existisse o modelo assíncrono.
Imagine este cenário sem assíncrono:
// Cenário hipotético — BLOQUEANTE (não é assim que funciona)
const dados = buscarDadosDoServidor(); // aguarda 3 segundos...
// Durante esses 3 segundos, NADA acontece na página
// O usuário não pode clicar, rolar, digitar — NADA
console.log(dados);
Uma página travada por 3 segundos é inaceitável. O JavaScript resolve isso com um modelo elegante: o Event Loop.
As peças do modelo assíncrono
Para entender o Event Loop, você precisa conhecer as peças:
┌─────────────────────────────────────────┐
│ CALL STACK │
│ (pilha de execução) │
│ [ função3 ] │
│ [ função2 ] │
│ [ função1 ] ← executa de cima │
│ [ main ] para baixo │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ WEB APIs / NODE APIs │
│ (fornecidas pelo ambiente, não pelo JS)│
│ setTimeout, fetch, DOM events, │
│ setInterval, XMLHttpRequest... │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ CALLBACK QUEUE │
│ (fila de callbacks) │
│ [ cb3, cb2, cb1 ] ← aguarda a vez │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ MICROTASK QUEUE │
│ (fila de microtarefas — prioridade) │
│ Promises, queueMicrotask │
└─────────────────────────────────────────┘
↑↑↑ EVENT LOOP ↑↑↑
Monitora a Call Stack e as filas.
Quando a Call Stack esvazia,
move callbacks para ela executar.
Call Stack — a pilha de execução
A Call Stack registra onde estamos na execução do programa. Toda vez que uma função é chamada, ela entra no topo da pilha. Quando retorna, sai.
function somar(a, b) {
return a + b; // 3. executa e sai da pilha
}
function calcular() {
const r = somar(3, 4); // 2. somar entra na pilha
console.log(r); // 4. console.log entra e sai
}
calcular(); // 1. calcular entra na pilha
Passo a passo da pilha:
[calcular] → calcular entra
[somar, calcular] → somar entra
[calcular] → somar retorna e sai
[console.log, calcular] → console.log entra e sai
[] → calcular termina e sai
Web APIs — delegando o trabalho pesado
Quando o JavaScript encontra uma operação demorada — como setTimeout, uma requisição HTTP ou um evento do DOM — ele delega para as Web APIs (fornecidas pelo navegador) e continua executando sem esperar:
console.log("1 — início");
setTimeout(() => {
console.log("3 — dentro do timeout");
}, 2000);
console.log("2 — fim");
// Saída:
// 1 — início
// 2 — fim
// (2 segundos depois...)
// 3 — dentro do timeout
O setTimeout foi delegado ao navegador. O JavaScript não esperou — continuou para o console.log("2"). Depois que o timer de 2 segundos acabou, o callback entrou na fila.
Event Loop — o orchestrador
O Event Loop tem uma tarefa simples mas fundamental:
ENQUANTO o programa estiver rodando:
SE a Call Stack estiver VAZIA:
SE a Microtask Queue tiver itens:
Mova TODOS para a Call Stack (um a um)
SENÃO SE a Callback Queue tiver itens:
Mova o PRIMEIRO para a Call Stack
É um loop infinito que fica observando. Quando a pilha esvazia, ele verifica as filas e move o próximo callback para execução.
Exemplo completo — rastreando o Event Loop
console.log("A");
setTimeout(() => console.log("B"), 0); // delay 0, mas ainda é assíncrono!
Promise.resolve().then(() => console.log("C")); // microtask
console.log("D");
// Saída: A, D, C, B
Por quê? Vamos rastrear:
1. console.log("A") → Call Stack executa → imprime "A"
2. setTimeout(cb, 0) → delegado às Web APIs → cb vai para Callback Queue
3. Promise.resolve().then() → microtask registrada → cb vai para Microtask Queue
4. console.log("D") → Call Stack executa → imprime "D"
Call Stack está vazia. Event Loop verifica:
→ Microtask Queue tem "C" → executa → imprime "C"
→ Microtask Queue vazia
→ Callback Queue tem "B" → executa → imprime "B"
Resultado: A, D, C, B
Microtasks sempre têm prioridade sobre macrotasks (callbacks de setTimeout/setInterval).
Microtasks vs Macrotasks
// MACROTASKS (Callback Queue)
// setTimeout, setInterval, setImmediate (Node), eventos DOM
// MICROTASKS (Microtask Queue — maior prioridade)
// Promise.then/catch/finally, queueMicrotask, MutationObserver
console.log("1");
setTimeout(() => console.log("2 — macrotask"), 0);
Promise.resolve()
.then(() => console.log("3 — microtask 1"))
.then(() => console.log("4 — microtask 2"));
queueMicrotask(() => console.log("5 — microtask 3"));
console.log("6");
// Saída: 1, 6, 3, 5, 4, 2
Todas as microtasks são processadas antes de qualquer macrotask — mesmo que a macrotask tenha sido agendada antes.
O que significa "bloquear a thread"
Agora que você entende o Event Loop, fica claro por que código síncrono pesado é um problema:
// Isso BLOQUEIA o Event Loop por vários segundos
// Durante esse tempo, nenhum evento, timer ou Promise pode executar
function calcularPesado() {
const inicio = Date.now();
while (Date.now() - inicio < 3000) {
// loop travado por 3 segundos
}
return "feito";
}
setTimeout(() => console.log("timeout"), 100); // deveria executar em 100ms
calcularPesado(); // bloqueia por 3 segundos
// O timeout só executa depois dos 3 segundos — não em 100ms
Por isso operações pesadas devem ser assíncronas — para não bloquear a thread e deixar a interface responsiva.
O problema dos callbacks aninhados
Antes das Promises, o código assíncrono usava apenas callbacks. Funcionava, mas gerava o temido Callback Hell:
// Callback Hell — código que cresce para a direita
obterUsuario(id, function(usuario) {
obterPedidos(usuario.id, function(pedidos) {
obterDetalhesPedido(pedidos[0].id, function(detalhes) {
calcularFrete(detalhes.endereco, function(frete) {
processarPagamento(frete, function(resultado) {
// chegamos ao inferno
if (resultado.erro) {
tratarErro(resultado.erro, function(r) {
// mais um nível...
});
}
});
});
});
});
});
Isso é difícil de ler, difícil de manter e difícil de tratar erros. As Promises foram criadas para resolver exatamente esse problema — e é o que estudamos no próximo artigo.
Visualizando o Event Loop na prática
// Vamos rastrear este código completo:
console.log("Script inicia"); // 1
setTimeout(() => { // agendado para depois
console.log("Timeout 1"); // 5
}, 0);
setTimeout(() => { // agendado para depois
console.log("Timeout 2"); // 6
}, 0);
Promise.resolve()
.then(() => {
console.log("Promise 1"); // 3
return "valor";
})
.then((val) => {
console.log("Promise 2:", val); // 4
});
console.log("Script termina"); // 2
// Saída:
// Script inicia
// Script termina
// Promise 1
// Promise 2: valor
// Timeout 1
// Timeout 2
setTimeout(fn, 0) — um truque útil
Como vimos, setTimeout com delay 0 não executa "agora" — executa após o código síncrono atual terminar. Isso é útil em alguns casos:
// Forçar o DOM a atualizar antes de executar algo pesado
button.textContent = "Carregando..."; // atualiza o texto
setTimeout(() => {
// O navegador teve chance de renderizar a atualização do DOM
// antes de executar este código pesado
processarDadosGrandes();
}, 0);
// Adiar execução para depois do Event Loop atual
console.log("síncrono 1");
setTimeout(() => console.log("assíncrono"), 0);
console.log("síncrono 2");
// síncrono 1 → síncrono 2 → assíncrono
Node.js e o Event Loop
O Node.js usa o mesmo conceito de Event Loop, mas com algumas adições específicas — como as fases do loop (timers, I/O, idle, poll, check, close). A ideia central é a mesma: uma thread, delegação de operações pesadas, callbacks em filas.
É por isso que o Node consegue ser eficiente mesmo sendo single-threaded — operações de I/O (leitura de arquivos, banco de dados, rede) são delegadas ao sistema operacional, e o Node continua processando outras requisições enquanto espera.
// Em Node.js — servidor que não bloqueia
const http = require("http");
http.createServer((req, res) => {
// Cada requisição chega como um evento
// O Node não fica esperado — processa várias ao mesmo tempo
res.end("Olá!");
}).listen(3000);
console.log("Servidor rodando na porta 3000");
// O Node não para aqui — fica ouvindo eventos indefinidamente
Resumo visual
┌─────────────────────────────────────────────────────┐
│ EXECUÇÃO JAVASCRIPT │
│ │
│ Código síncrono → Call Stack (imediato) │
│ │
│ setTimeout/setInterval → Web API → Callback Queue │
│ (macrotask — executa quando Call Stack vazia) │
│ │
│ Promise.then/catch → Microtask Queue │
│ (prioridade — executa ANTES das macrotasks) │
│ │
│ EVENT LOOP monitora tudo e gerencia as filas │
└─────────────────────────────────────────────────────┘
Ordem de prioridade:
1. Código síncrono (Call Stack)
2. Microtasks (Promises)
3. Macrotasks (setTimeout, eventos)
Tarefa para você
Sem executar o código, tente prever a saída de cada exemplo:
Desafio 1:
console.log("A");
setTimeout(() => console.log("B"), 1000);
setTimeout(() => console.log("C"), 0);
console.log("D");
// Qual a ordem? A, D, C, B ou A, D, B, C ou outra?
Desafio 2:
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve()
.then(() => {
console.log("3");
setTimeout(() => console.log("4"), 0);
})
.then(() => console.log("5"));
console.log("6");
// Qual a ordem?
Desafio 3: Explique com suas palavras por que uma animação CSS é preferível a uma animação feita com setInterval quando há código JavaScript pesado rodando ao mesmo tempo.
Conclusão
Neste artigo você aprendeu:
- Por que JavaScript é single-threaded e por que isso não é um problema
- O que é a Call Stack e como funciona
- O papel das Web APIs na delegação de tarefas
- A Callback Queue e a Microtask Queue
- Como o Event Loop orquestra tudo
- A diferença entre microtasks e macrotasks
- Por que código síncrono pesado bloqueia a interface
- O problema do Callback Hell que levou à criação das Promises
- Como o Node.js usa o mesmo modelo
📚 Fontes e Referências
- MDN Web Docs — Concurrency model and Event Loop: https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Event_loop
- MDN Web Docs — Microtask Guide: https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide
- JavaScript.info — Event Loop: https://javascript.info/event-loop
- Jake Archibald — Tasks, microtasks, queues and schedules: https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules
- Philip Roberts — What the heck is the Event Loop? (JSConf EU): https://www.youtube.com/watch?v=8aGhZQkoFbQ
- Loupe — Visualizador interativo do Event Loop: http://latentflip.com/loupe
- You Don't Know JS: Async & Performance — Kyle Simpson: https://github.com/getify/You-Dont-Know-JS
- Eloquent JavaScript, Cap. 11 — Asynchronous Programming: https://eloquentjavascript.net/11_async.html