Destino Dev

Blog

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 é:

  1. inicio
  2. fim
  3. microtask: promise.then
  4. 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

  1. Reproduza exemplos que misturam async/await, Promise.resolve().then e setTimeout para consolidar ordem de execução.
  2. Implemente uma versão do simulador que mostre timestamps e duração de execução para estudar latência.
  3. 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