// js
O que é o event loop em JavaScript? O assíncrono explicado
O JavaScript roda em uma única thread e, ainda assim, lida com timers, rede e cliques sem travar. O event loop é a chave: a call stack, as filas de tasks e microtasks, e por que as promises rodam antes do setTimeout.
O JavaScript tem uma peculiaridade famosa: ele executa seu código em uma única thread, fazendo uma coisa de cada vez — e, mesmo assim, consegue aguardar uma requisição de rede, rodar um timer e responder a cliques sem nunca travar. O que torna isso possível é o event loop. Entendê-lo é o que transforma o JavaScript assíncrono de confuso em óbvio.
Uma thread, uma call stack
O JavaScript executa o código em uma única call stack (pilha de chamadas): as funções são empilhadas quando chamadas e desempilhadas quando retornam, uma de cada vez. Como só existe uma pilha, apenas um trecho de código roda a qualquer momento. Se uma função demora muito, nada mais — nem mesmo o clique em um botão — pode rodar até que ela termine. É por isso que um laço pesado «trava» a página.
Então, como o JavaScript aguarda um timer de 2 segundos ou um download lento sem bloquear tudo? O truque é que ele não aguarda de jeito nenhum. Ele entrega essas tarefas ao ambiente que o cerca e segue em frente.
É o ambiente que aguarda
O navegador (e o Node.js) dá ao JavaScript poderes adicionais que não fazem parte da linguagem em si: timers, requisições de rede, acesso a arquivos, listeners de eventos. Quando você chama setTimeout ou fetch, o motor entrega essa tarefa ao ambiente e segue adiante imediatamente. O ambiente aguarda em segundo plano e, quando o trabalho termina, coloca seu callback em uma fila (queue) — e não diretamente de volta na pilha.
O que o event loop realmente faz
É aqui que entra o event loop. Sua tarefa é simples de enunciar: sempre que a call stack estiver vazia, pegar o próximo callback da fila e empilhá-lo para executar. Ele roda em laço indefinidamente, verificando «a pilha está vazia? há algo aguardando?» e injetando os callbacks da fila um de cada vez. Essa única regra é todo o mecanismo por trás do JavaScript assíncrono.
A consequência fundamental: seus callbacks na fila só rodam depois que todo o código síncrono atual termina. Um setTimeout(fn, 0) não roda «agora» — ele roda assim que a pilha fica livre, mesmo que isso seja mais tarde do que você espera.
Microtasks vs macrotasks: por que as promises vencem
Não há uma única fila, mas (pelo menos) duas, e a diferença explica a pegadinha mais comum em entrevistas. As macrotasks incluem coisas como setTimeout e callbacks de I/O. As microtasks incluem callbacks de promises resolvidas (.then) e queueMicrotask. A regra é que, após cada macrotask, o event loop esvazia a fila de microtasks inteira antes de prosseguir.
É por isso que isto imprime start, end, promise, timeout — a promise roda antes do timer, mesmo que ele esteja definido como 0:
console.log('start');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('end'); As duas chamadas a console.log são síncronas e rodam primeiro. O callback da promise é uma microtask e roda assim que a pilha se libera. O timeout é uma macrotask e aguarda sua vez após as microtasks.
Por que isso importa na prática
O event loop explica os comportamentos que fazem as pessoas tropeçarem: por que setTimeout(fn, 0) significa «o mais cedo possível» e não «imediatamente», por que um cálculo síncrono longo deixa a página inteira sem resposta, e por que o código async/await só retoma depois que o trabalho síncrono ao redor termina. Assim que você visualiza a pilha se esvaziando e o loop injetando os callbacks da fila, o JavaScript assíncrono deixa de ser um mistério.
Node.js vs o navegador
O conceito é o mesmo no Node.js, mas o host é diferente: em vez das Web APIs do navegador, o Node usa uma biblioteca chamada libuv para lidar com timers, sistema de arquivos e rede fora da thread principal. O loop do Node tem algumas fases extras (e o process.nextTick fica até antes das microtasks normais), mas o modelo mental é idêntico — uma thread, um ambiente que aguarda e um loop que reinjeta o trabalho concluído.
Conclusão
O event loop é como o JavaScript de thread única se mantém responsivo: o código síncrono roda na call stack, o trabalho lento é entregue ao ambiente, e os callbacks concluídos aguardam em filas até que a pilha esteja vazia. As microtasks (promises) rodam antes das macrotasks (timers), e nada de assíncrono jamais interrompe um código que já está rodando. Guarde essa imagem e o resto do JavaScript assíncrono — promises, async/await, timers — se encaixa no lugar.
Perguntas frequentes
- O que é o event loop em JavaScript?
- É o mecanismo que mantém o JavaScript, de thread única, responsivo: assim que a call stack fica vazia, o event loop pega o próximo callback na fila e o executa. O trabalho lento (timers, requisições de rede) é entregue ao ambiente, que enfileira o seu callback quando termina.
- Por que uma promise roda antes do setTimeout?
- Os callbacks de promise são microtasks e os timers são macrotasks. Depois de cada macrotask, o event loop esvazia primeiro toda a fila de microtasks, então o .then de uma promise resolvida roda antes de um callback setTimeout(…, 0), mesmo que o timer estivesse em zero.
- O setTimeout(fn, 0) roda imediatamente?
- Não. Ele roda assim que a call stack fica livre e depois de quaisquer microtasks pendentes, não instantaneamente. Zero significa o mais rápido possível, o que ainda é depois de o código síncrono atual terminar.
- O event loop é igual no Node.js e no navegador?
- O modelo mental é o mesmo — uma thread, o ambiente espera e um loop reinjeta os callbacks concluídos. O Node.js usa libuv e tem algumas fases extras (e process.nextTick vem antes dos microtasks normais), mas a ideia central é idêntica.