Criando um simulador interativo do Event Loop: Call Stack, Tasks e Microtasks
Por John Pires · 14 de março de 2026

Criando um simulador interativo do Event Loop: Call Stack, Tasks e Microtasks
1) Por que aprender isso em 2026
O JavaScript continua dominando tanto o front-end quanto partes do back-end. Entender como o event loop funciona deixou de ser detalhe teórico: é necessário para depurar problemas de performance, evitar race conditions e escrever APIs reativas corretas. Em 2026, com cada vez mais aplicações client-side ricas, workers, webassembly e programação reativa, dominar a interação entre call stack, tasks e microtasks faz diferença prática na arquitetura de uma aplicação.
Este artigo investiga o problema: por que alguns console.log aparecem fora da ordem esperada? Por que um Promise.then é executado antes de um setTimeout com delay zero? Vamos explorar alternativas conceptuais e, no final, construir um simulador interativo simples que deixa explícito o fluxo de execução.
Não é só teoria. O objetivo é que, após seguir o passo a passo, você entenda o que acontece no runtime e consiga reproduzir e prever o comportamento em código real.
O problema prático
Muitos iniciantes acreditam que JavaScript é estritamente single-threaded sequencial. Isso é verdade para a call stack, mas o runtime adiciona filas assíncronas (tasks) e microtasks que mudam a ordem de execução. Saber distinguir cada camada evita bugs sutis.
Como este artigo aborda o tema
Adotamos um tom investigativo: apresentamos exemplos que quebram expectativas, comparamos alternativas e concluímos construindo um simulador interativo. Cada trecho de código vem com contexto e explicação do resultado.
2) Configuracao do ambiente
Você precisa de um navegador moderno (Chrome, Firefox, Edge) ou Node.js v20+. O simulador final usa DOM, portanto é ideal rodar num arquivo HTML simples ou em um snippet no DevTools do navegador. Para testar exemplos de console, qualquer console JS serve.
Recomendações práticas:
- Use um editor de código leve (VS Code, Sublime) e abra a janela DevTools do navegador.
- Caso use Node, lembre que APIs como queueMicrotask existem, mas algumas diferenças ocorrem entre browser e Node (por exemplo, process.nextTick no Node é similar a microtasks).
- Mantenha o console visível e limpo antes de cada experimento para observar logs sem ruído.
Estrutura dos exemplos
Cada bloco de código neste artigo tem: 1) contexto antes (o que faz), 2) código, 3) explicação depois (o que acontece e por quê). Isso ajuda na experimentação passo a passo.
Ferramentas extras
Se quiser gravar e compartilhar suas experiências, use ferramentas como CodePen, JSFiddle ou um repo simples no GitHub Pages. O simulador pode ser copiado para um index.html e aberto localmente.
3) Hello World progressivo
Vamos começar com um exemplo mínimo que costuma confundir iniciantes: Promise.then versus setTimeout. Observe a ordem de execução e depois discutimos por que isso acontece.
Contexto: o código loga mensagens em sequência, mistura setTimeout com Promise.resolve().then e mostra a ordem final no console.
console.log('inicio');
setTimeout(function task() {
console.log('task: setTimeout 0');
}, 0);
Promise.resolve().then(function micro() {
console.log('microtask: promise.then');
});
console.log('fim');
O que acontece: a saída esperada e real geralmente é:
- inicio
- fim
- microtask: promise.then
- task: setTimeout 0
Por quê: o call stack executa os logs síncronos 'inicio' e 'fim'. Quando encontra Promise.resolve().then, cria uma microtask que vai para a fila de microtasks. A fila de microtasks é esvaziada antes que o event loop pegue a próxima task (ou callback de timer). Assim, microtask roda antes do setTimeout.
Essa distinção entre task (fila de macrotasks, ex: setTimeout, I/O, UI events) e microtask (Promise callbacks, queueMicrotask, MutationObserver) é a chave para prever a ordem.
Exemplo com queueMicrotask
Contexto: usamos queueMicrotask para agendar algo explicitamente na fila de microtasks e comparamos novamente com setTimeout.
console.log('start');
queueMicrotask(function q() {
console.log('microtask: queueMicrotask');
});
setTimeout(function t() {
console.log('macrotask: setTimeout');
}, 0);
console.log('end');
O que acontece: a saída segue a mesma lógica — microtask antes de macrotask. Use queueMicrotask quando quiser garantir execução logo após o código síncrono, mas antes do próximo tick de eventos.
Dica rápida
Microtasks podem bloquear UI se você enfileirar muitos callbacks sem permitir que o navegador processe repaints. Use com cuidado.
4) Mini-projeto pratico
Objetivo: construir um simulador interativo simples que mostra o estado do call stack, fila de tasks e fila de microtasks. Ele será uma página HTML com botões para empurrar tarefas/microtasks e um visualizador em tempo real. O código é didático e pode ser estendido.
Decisão de design: não vamos reimplementar o event loop do browser. Em vez disso, modelamos estruturas que ajudam a entender o fluxo. Haverá três coleções: callStack (array), microtasks (array), tasks (array). O runtime real do navegador interage com essas coleções; nosso simulador apenas reflete a ordem e permite triggers controlados.
Antes do bloco de código, saiba que este HTML pode ser salvo como index.html e aberto no navegador.
/* Salve como index.html e abra no navegador.
O código cria botões para adicionar task/microtask e processa-os de forma semelhante ao event loop didático. */
const logEl = document.createElement('pre');
document.body.appendChild(logEl);
const state = {
callStack: [],
microtasks: [],
tasks: []
};
function render() {
logEl.textContent = 'CallStack: ' + JSON.stringify(state.callStack) + '\n' +
'Microtasks: ' + JSON.stringify(state.microtasks) + '\n' +
'Tasks: ' + JSON.stringify(state.tasks) + '\n';
}
function pushTask(fn) {
state.tasks.push(fn);
render();
}
function pushMicrotask(fn) {
state.microtasks.push(fn);
render();
}
// Processa um ciclo: esvazia microtasks, depois executa uma task
function processOnce() {
// process microtasks até acabar
while (state.microtasks.length) {
const mt = state.microtasks.shift();
state.callStack.push('microtask');
render();
mt();
state.callStack.pop();
}
// process one task (macrotask)
const task = state.tasks.shift();
if (task) {
state.callStack.push('task');
render();
task();
state.callStack.pop();
}
render();
}
// UI
const btnTask = document.createElement('button');
btnTask.textContent = 'Adicionar task (setTimeout)';
btnTask.onclick = () => pushTask(() => console.log('executando task'));
document.body.appendChild(btnTask);
const btnMicro = document.createElement('button');
btnMicro.textContent = 'Adicionar microtask (promise.then)';
btnMicro.onclick = () => pushMicrotask(() => console.log('executando microtask'));
document.body.appendChild(btnMicro);
const btnStep = document.createElement('button');
btnStep.textContent = 'Processar um ciclo';
btnStep.onclick = processOnce;
document.body.appendChild(btnStep);
render();
O que acontece: ao clicar em 'Adicionar microtask', você coloca um callback na fila microtasks; 'Adicionar task' coloca na fila tasks. 'Processar um ciclo' simula o comportamento do event loop: esvazia microtasks primeiro, depois executa UMA task.
Use o console do navegador para ver os logs de execução e o painel para ver o estado. Experimente enfileirar múltiplas microtasks antes de uma task e observe que todas as microtasks rodam antes da task ser processada.
Extensões práticas
Você pode estender o simulador para:
- Adicionar timers reais via setTimeout para agendar tasks com atraso.
- Mostrar stack traces das funções empilhadas.
- Simular prioridades ou múltiplas filas de tasks.
Limitações
Este simulador é didático: não substitui o comportamento real do runtime. Por exemplo, em navegadores, a fila de tasks inclui eventos de UI e repaints e existe interação complexa com a API de renderização.
Erros comuns e armadilhas
Nesta seção compilamos pegadinhas que vi repetidamente em código de iniciantes. Saber evitá-las salva horas de debugging.
Armadilha 1: confiar na ordem de setTimeout 0
Muitos acreditam que setTimeout(fn, 0) roda imediatamente após o código síncrono. Na prática, ele aciona uma task que só corre depois que todas as microtasks do tick atual terminarem. Se você precisa de execução imediata após o contexto atual, use queueMicrotask ou Promise.resolve().then.
Armadilha 2: microtasks infinitas bloqueiam UI
Enfileirar microtasks indefinidamente (por exemplo, em um loop que chama queueMicrotask repetidamente) impede que o browser realize repaints ou processe eventos de input. Isso gera congelamento da interface.
Armadilha 3: confusão entre async/await e microtasks
async/await é sintax sugar sobre Promises. O ponto importante: o corpo do async antes do primeiro await roda de forma síncrona; após um await, a continuação é uma microtask (Promise.then). Por isso, await também produz comportamento de microtask.
Teste sempre esses casos com logs controlados e limpe o console entre execuções para não confundir saídas anteriores.
5) Recursos para continuar estudando
Depois de construir e brincar com o simulador, recomenda-se aprofundar-se nas especificações e materiais práticos. Listo referências e exercícios para fixar.
Leituras fundamentais
- ECMAScript Language Specification: seção sobre job queues e microtasks (jobs).
- WHATWG HTML Living Standard: event loop processing model.
- Artigos técnicos de desenvolvedores do Chrome/Firefox sobre microtasks e timers.
Exercícios práticos
- Reproduza exemplos que misturam async/await, Promise.resolve().then e setTimeout para consolidar ordem de execução.
- Implemente uma versão do simulador que mostre timestamps e duração de execução para estudar latência.
- Investigue diferenças entre browser e Node.js (process.nextTick, setImmediate).
Seguindo esses exercícios você transforma o conceito abstrato em intuição prática.
Conclusão prática
Construir um simulador interativo do event loop força você a nomear e visualizar as partes: call stack, microtasks e tasks. A prática reduz a surpresa quando uma callback roda antes de outra. Em 2026, com aplicações cada vez mais reativas e concorrentes em nível de UI, essa habilidade é essencial.
Resumo dos pontos chave:
- Call stack executa código síncrono imediatamente.
- Microtasks (Promises, queueMicrotask) rodam após o stack esvaziar, antes da próxima task.
- Tasks (timers, I/O, eventos) são processadas em ticks distintos do event loop.
Próximos passos: salve o simulador como página estática, modifique-o para adicionar mais visibilidade para repaints e experimente cenários reais da sua aplicação para detectar problemas de ordem de execução.
Boa prática: sempre instrumente código assíncrono com logs e, quando necessário, escreva testes que cubram a ordem esperada de execução entre sync, microtasks e tasks.
Se quiser, posso fornecer uma versão mais completa do simulador com CSS, timeline visual e exportação dos logs para análise posterior.
Assuntos: JavaScript, event-loop, promises, microtasks, beginners
Leia também
Implementando Token Bucket no Service Worker para controlar fetchs entre abas
Token Bucket no Service Worker: controle de fetchs entre abas com estado centralizado, refill baseado em timestamps, enfileiramento seguro e persistência em…
Web Components semânticos para formulários: labels, validação e acessibilidade
Padrões práticos para construir Web Components de formulário mantendo labels, validação e acessibilidade: quando usar Shadow DOM, ElementInternals e fallbacks…
Web Components semânticos: Shadow DOM acessível e indexável para SEO
Padrões práticos para criar Web Components semânticos: mantenha semântica no light DOM, use Shadow DOM só para estilo, ou implemente ElementInternals com…
Retry resiliente com fetch: AbortController, backoff exponencial e streaming
Padrões práticos para retries resilientes com fetch: combine AbortController para cancelamento, backoff exponencial com jitter para evitar picos, e streaming…
WebAssembly com Rust para o navegador: compilar e usar com JS (iniciantes)
Guia prático para iniciantes: compile Rust para WebAssembly e consuma no navegador via JS. Passo a passo para tokenizacao, cálculo de scores e integração com…
Pool de concorrência com Async Iterators e AbortSignal
Implementar um pool de concorrência com Async Iterators e AbortSignal para controlar tarefas assíncronas, permitir cancelamento cooperativo e aplicar…