Event Loop no Node.js: O DJ Que Nunca Para de Tocar
Node.js é single-threaded. Isso significa que ele só faz uma coisa por vez. E mesmo assim, consegue lidar com milhares de conexões simultâneas sem suar. Como?
A resposta é o Event Loop — e entendê-lo vai mudar a forma como você pensa sobre código assíncrono para sempre.
Vamos percorrer isso usando a analogia de um DJ numa festa. Fica comigo.
A Mesa do DJ (Call Stack)
Imagine um DJ numa festa. A mesa do DJ (a Call Stack) só consegue tocar uma música por vez. É isso. Não importa quantos pedidos chegam, o DJ processa um por um, em ordem.
Em termos de JavaScript, essa é a call stack — uma estrutura LIFO (Last In, First Out) onde funções são empilhadas quando chamadas e removidas quando retornam.
function receberConvidado(nome) {
console.log(`Bem-vindo, ${nome}!`);
}
function abrirFesta() {
receberConvidado("Alice");
receberConvidado("Bob");
}
abrirFesta();
// Saída:
// Bem-vindo, Alice!
// Bem-vindo, Bob!
Cada chamada de função é empilhada, executada e removida antes da próxima rodar. Simples, sequencial, previsível.
Os Roadies (Internals do Node / libuv)
O DJ não pode sair da mesa para configurar a máquina de fumaça. Esse é o trabalho dos roadies.
No Node.js, os roadies são os internals do Node (libuv e a camada de async do sistema operacional — equivalente ao que os navegadores chamam de Web APIs): setTimeout, fetch, leitura de arquivos, requisições de rede. Quando você chama uma dessas, o Node passa a tarefa para o ambiente e continua imediatamente — o DJ continua tocando enquanto os roadies fazem o trabalho deles.
console.log("A festa começa");
setTimeout(() => {
console.log("Máquina de fumaça pronta!");
}, 2000);
console.log("O DJ já está tocando");
// Saída:
// A festa começa
// O DJ já está tocando
// Máquina de fumaça pronta! (2 segundos depois)
O Node não parou e esperou 2 segundos. Delegou para o roadie e seguiu em frente. Isso é I/O não-bloqueante.
A Fila de Pedidos (Macrotask Queue)
Quando um roadie termina o trabalho, o resultado não vai direto para a mesa do DJ. Ele vai para a fila de pedidos — a Macrotask Queue.
O Event Loop (a disciplina do DJ) tem uma regra: só pegar da fila quando a mesa estiver completamente livre.
console.log("1 - DJ abre o set");
setTimeout(() => console.log("3 - Primeiro pedido da fila"), 0);
setTimeout(() => console.log("4 - Segundo pedido da fila"), 0);
console.log("2 - DJ termina a abertura");
// Saída:
// 1 - DJ abre o set
// 2 - DJ termina a abertura
// 3 - Primeiro pedido da fila
// 4 - Segundo pedido da fila
Mesmo com setTimeout(..., 0), os callbacks passam pela fila e só rodam depois que a call stack atual esvazia.
Exemplos de macrotasks: setTimeout, setInterval, callbacks de I/O, setImmediate (Node.js).
A Fila VIP (Microtask Queue)
Aqui é onde fica interessante.
Alguns convidados da festa são VIPs. Eles têm uma fila separada, e o DJ tem uma política rígida: após cada música (macrotask), esvaziar toda a fila VIP antes de tocar a próxima música da fila normal. Na verdade, a fila VIP também é processada logo que a call stack inicial esvazia — antes mesmo da primeira música da fila normal tocar.
A Microtask Queue funciona exatamente assim. Ela é processada completamente após cada macrotask, antes da próxima macrotask começar.
console.log("1 - Início");
setTimeout(() => console.log("5 - Macrotask (setTimeout)"), 0);
Promise.resolve()
.then(() => console.log("3 - Microtask 1 (Promise)"))
.then(() => console.log("4 - Microtask 2 (Promise encadeada)"));
console.log("2 - Fim do código síncrono");
// Saída:
// 1 - Início
// 2 - Fim do código síncrono
// 3 - Microtask 1 (Promise)
// 4 - Microtask 2 (Promise encadeada)
// 5 - Macrotask (setTimeout)
Os callbacks das Promises (microtasks) sempre rodam antes do callback do setTimeout (macrotask), mesmo que o setTimeout tenha sido registrado primeiro.
Exemplos de microtasks: Promise.then, Promise.catch, Promise.finally, queueMicrotask, continuações de async/await.
O Set Completo
Vamos juntar tudo com um exemplo completo:
console.log("1 - Síncrono: a festa começa");
setTimeout(() => console.log("6 - Macrotask: trabalho do roadie concluído"), 0);
Promise.resolve()
.then(() => {
console.log("4 - Microtask: convidado VIP chega");
})
.then(() => console.log("5 - Microtask: VIP recebe sua bebida"));
setTimeout(() => console.log("7 - Macrotask: segundo roadie concluído"), 0);
console.log("2 - Síncrono: DJ continua tocando");
console.log("3 - Síncrono: call stack esvazia");
// Saída:
// 1 - Síncrono: a festa começa
// 2 - Síncrono: DJ continua tocando
// 3 - Síncrono: call stack esvazia
// 4 - Microtask: convidado VIP chega
// 5 - Microtask: VIP recebe sua bebida
// 6 - Macrotask: trabalho do roadie concluído
// 7 - Macrotask: segundo roadie concluído
Ordem de execução:
- Todo o código síncrono (call stack esvazia completamente)
- Todas as microtasks (fila VIP — esvaziada completamente)
- Uma macrotask (fila normal)
- Todas as microtasks novamente (fila VIP — esvaziada novamente)
- Próxima macrotask... e assim por diante
3 Regras de Ouro
- Uma coisa por vez. A call stack é single-threaded — sem paralelismo — ela lida com muitas tarefas delegando, não fazendo tudo ao mesmo tempo.
- Microtasks sempre vêm antes das macrotasks. Após cada macrotask, esvaziar toda a fila de microtasks primeiro.
- Operações assíncronas não bloqueiam. Elas são delegadas ao ambiente (roadies), liberando a call stack para continuar trabalhando.
Entender essas três regras explica 90% do comportamento "estranho" que você vai encontrar com JavaScript assíncrono no Node.js.