Microtasks rodam entre fases do event loop, antes que a próxima fase comece — código síncrono → nextTick → microtasks → event loop phase. A hierarquia de prioridade é estrita: process.nextTick drena sua fila inteira primeiro (incluindo novos nextTick adicionados durante a drenagem), depois queueMicrotask e Promise.then são processados na fila padrão de microtasks em ordem FIFO.
Recursão em nextTickbloqueia o loop indefinidamente — o fenômeno chamado de queue starvation; use setImmediate para trabalho fatiado. nextTick é Node-specific; queueMicrotask é portável para browsers, Deno e Bun. A diferença entre as três APIs não é só de prioridade: erros em nextTick/queueMicrotask viram uncaughtException, enquanto erros em Promise.then viram unhandledRejection — categorias de falha distintas com handlers distintos.
Por que process.nextTick às vezes dispara antes das suas Promises?
Você tem código async com Promise.then e process.nextTick e a ordem de execução não é o que você esperava. Ou pior — um servidor em produção trava misteriosamente sem consumir CPU, e nenhum log aparece. As duas situações têm a mesma raiz: existem três filas diferentes que parecem iguais mas têm prioridades estritamente ordenadas.
O que é
Uma microtask é uma unidade de trabalho que o runtime executa assim que o código síncrono atual termina, mas antes de avançar para a próxima fase do event loop (antes de processar timers, I/O, setImmediate, etc.). O conceito existe tanto no browser quanto no Node.js, mas o Node adiciona uma camada extra: a fila do process.nextTick.
No Node.js, existem três APIs principais que agendam trabalho nesse espaço “entre fases”:
process.nextTick(callback[, ...args])
A API mais antiga e de maior prioridade. Callbacks registrados com nextTick são colocados em uma fila separada — a nextTickQueue — que é drenada completamente antes que qualquer microtask convencional seja processada, e antes que o event loop avance de fase. É uma API Node-specific: não existe nos browsers nem no Deno com a mesma semântica.
O segundo argumento em diante é repassado como argumento ao callback — útil para evitar closures desnecessárias:
process.nextTick((usuario, acao) => { console.log(`${usuario} fez ${acao}`);}, 'alice', 'login');
queueMicrotask(callback)
Introduzida no Node.js 11 (globalizada no Node 12), queueMicrotask agenda uma função na fila padrão de microtasks — a mesma fila usada pelos callbacks de Promise. É a API portável: existe no browser, no Node.js, no Deno e no Bun com comportamento idêntico. Não aceita argumentos extras (use uma arrow function se precisar de closure).
queueMicrotask(() => { console.log('microtask via queueMicrotask');});
Promise.resolve().then(callback)
O mecanismo mais familiar. Callbacks de .then(), .catch() e .finally() também são enfileirados na fila padrão de microtasks, na mesma posição que queueMicrotask. A diferença principal está no tratamento de erros: erros em callbacks de Promise viram unhandledRejection; erros em queueMicrotask viram uncaughtException.
Promise.resolve().then(() => { console.log('microtask via Promise.then');});
Hierarquia dentro de uma iteração
Em cada ponto de drenagem (ao final de cada fase do event loop, e ao final de cada callback de macrotask):
nextTickQueue — drenada completamente (todos os nextTick pendentes, incluindo os novos que forem adicionados durante a drenagem)
Microtask queue — drenada completamente (queueMicrotask + Promise.then, intercalados na ordem em que foram enfileirados)
Próxima fase do event loop começa
Diagrama — prioridade das filas
flowchart LR
CS["Código síncrono\n(Call Stack)"]
CS -->|"stack vazia"| NT
subgraph MICRO["Espaço entre fases do event loop"]
direction TB
NT["nextTickQueue\n(process.nextTick)\n🔴 maior prioridade"]
MT["Microtask Queue\n(queueMicrotask + Promise.then)\n🟡 prioridade padrão"]
NT -->|"esgota completamente"| MT
end
MT -->|"esgota completamente"| EL
EL["Próxima fase do\nEvent Loop\n(timers / poll / check…)"]
Regra crítica:nextTick registrado dentro de outra microtask drena antes das demais microtasks já enfileiradas — a nextTickQueue é verificada após cada microtask individual.
Por que importa
Controlar a ordem de execução em nível fino é essencial em dois contextos principais:
Correção de APIs assíncronas: uma função que às vezes retorna sincronamente e às vezes assincronamente cria bugs difíceis de rastrear. Deferir o callback com nextTick garante que o caller sempre recebe o resultado de forma assíncrona, mesmo quando o trabalho é síncrono — permitindo que o caller registre listeners ou configure estado antes que o callback rode.
Debugging de ordem de execução: microtasks são a causa mais comum de “por que esse callback rodou antes do que eu esperava?“. Entender a hierarquia — nextTick > microtasks > macrotasks (timers, I/O, setImmediate) — é requisito para diagnosticar corridas e comportamentos inesperados em código assíncrono.
Como funciona
Exemplo 1 — Ordem de execução com timer, Promise e nextTick
setTimeout(() => console.log('1: timer'), 0);Promise.resolve().then(() => console.log('2: promise'));process.nextTick(() => console.log('3: nextTick'));console.log('4: síncrono');// Saída:// 4: síncrono (código síncrono roda primeiro, esvaziando a call stack)// 3: nextTick (nextTickQueue drenada antes das microtasks)// 2: promise (microtask queue drenada antes de avançar de fase)// 1: timer (macrotask — fase timers do event loop)
A call stack deve estar vazia antes de qualquer microtask rodar. Código síncrono tem prioridade absoluta. Em seguida vem a nextTickQueue, depois as microtasks convencionais, e só então o event loop avança para a próxima fase (onde timers, I/O e setImmediate vivem).
Exemplo 2 — Recursão em nextTick bloqueia o event loop (starvation)
function loop() { process.nextTick(loop);}loop();// O programa nunca avança.// Nenhum timer, I/O, ou setImmediate jamais executa.// A nextTickQueue é drenada antes de qualquer avanço de fase,// mas cada drenagem adiciona mais um item — loop infinito na fila.
O mesmo problema ocorre com queueMicrotask recursivo: a microtask queue também é completamente drenada antes de avançar de fase, então recursão na microtask queue também trava o event loop.
Exemplo 3 — queueMicrotask vs Promise.resolve().then
queueMicrotask(() => console.log('A: queueMicrotask'));Promise.resolve().then(() => console.log('B: Promise.then'));queueMicrotask(() => console.log('C: queueMicrotask 2'));// Saída:// A: queueMicrotask// B: Promise.then// C: queueMicrotask 2// Ambas as APIs alimentam a mesma fila de microtasks.// A ordem é a ordem de enfileiramento — FIFO.
A diferença não é de prioridade, mas de comportamento em caso de erro:
// Erro em queueMicrotask → uncaughtExceptionqueueMicrotask(() => { throw new Error('erro síncrono na microtask');});// Erro em Promise.then → unhandledRejectionPromise.resolve().then(() => { throw new Error('erro vira rejeição de Promise');});
Tabela comparativa
API
Fila
Prioridade
Padrão
Aceita args extras
Erro não capturado
process.nextTick
nextTickQueue
Mais alta
Node-specific
Sim
uncaughtException
queueMicrotask
Microtask queue
Padrão
Web/Node/Deno/Bun
Não
uncaughtException
Promise.then
Microtask queue
Padrão
Web/Node/Deno/Bun
Não
unhandledRejection
Na prática
Pattern: EventEmitter no constructor
O uso canônico do process.nextTick em bibliotecas é garantir que o evento emitido no construtor possa ser observado por listeners registrados depois da instanciação:
const { EventEmitter } = require('node:events');class MyEmitter extends EventEmitter { constructor() { super(); // SEM nextTick: o emit roda antes do caller registrar .on('event', ...) // COM nextTick: o emit é diferido, caller tem chance de registrar o listener process.nextTick(() => { this.emit('ready'); }); }}const emitter = new MyEmitter();// Este listener é registrado antes do nextTick rodaremitter.on('ready', () => { console.log('emitter pronto'); // funciona corretamente});
Sem o nextTick, o emit('ready') rodaria durante a execução do construtor, antes que a linha emitter.on('ready', ...) fosse alcançada — e o listener nunca seria disparado.
Pattern: validação assíncrona consistente
function processarDados(dados, callback) { if (!Array.isArray(dados)) { // Retornar erro sincronamente quebraria o contrato assíncrono da API return process.nextTick(callback, new TypeError('dados deve ser array')); } // Caminho assíncrono real setImmediate(() => { callback(null, dados.map(d => d * 2)); });}// O caller sempre recebe o callback de forma assíncrona — comportamento previsívelprocessarDados('erro', (err) => { console.error(err.message); // 'dados deve ser array'});
Quando preferir queueMicrotask
Use queueMicrotask quando:
O código precisa rodar em múltiplos runtimes (browser + Node.js)
Não há necessidade de prioridade sobre Promises
A semântica padrão de microtask é suficiente
Use process.nextTick quando:
É necessária execução antes de qualquer Promise pendente
O código é Node.js-only e precisa do padrão de callback assíncrono consistente
Está construindo uma API de baixo nível que emite eventos
Casos práticos
Cenário 1 — EventEmitter no constructor: o clássico uso de nextTick
Uma biblioteca criava um EventEmitter e emitia 'ready' no construtor. Usuários reportavam que o listener nunca disparava.
const { EventEmitter } = require('node:events');// ❌ Sem nextTick: emit roda DURANTE o construtor, antes do caller registrar .on()class BadEmitter extends EventEmitter { constructor() { super(); this.emit('ready'); // caller ainda não registrou o listener — silêncio }}const bad = new BadEmitter();bad.on('ready', () => console.log('nunca chega aqui')); // tarde demais// ✅ Com nextTick: emit é diferido para depois que o caller termina de configurarclass GoodEmitter extends EventEmitter { constructor() { super(); process.nextTick(() => this.emit('ready')); // drena após o código síncrono atual }}const good = new GoodEmitter();good.on('ready', () => console.log('disparou!')); // registrado antes do nextTick rodar
Por que funciona:nextTick drena após a call stack esvaziar — o caller tem a oportunidade de registrar listeners antes do emit.
Cenário 2 — Processamento de fila longa: nextTick vs setImmediate
Um worker processava filas de mensagens usando process.nextTick para encadear itens. Em produção, requests HTTP ao mesmo servidor passaram a expirar com timeout.
// ❌ nextTick recursivo: starvation — nenhum I/O executa enquanto a fila não terminarfunction processarFila(items) { if (!items.length) return; processar(items.shift()); process.nextTick(() => processarFila(items)); // enfileira mais um nextTick}// 10.000 itens = 10.000 nextTicks antes que qualquer request HTTP seja atendida// ✅ setImmediate: fatia o trabalho, libera o loop entre cada itemfunction processarFilaSegura(items) { if (!items.length) return; processar(items.shift()); setImmediate(() => processarFilaSegura(items)); // fase check — libera poll entre fatias}// O loop pode atender I/O entre cada item da fila
Diferença de impacto:nextTick tem prioridade absoluta sobre fases do loop; setImmediate roda na fase check, depois de I/O, cedendo espaço para requests.
Armadilhas comuns
nextTick recursivo causa starvation — o processo parece vivo mas está travado
A nextTickQueue é drenada completamente antes de qualquer fase avançar. Um nextTick que agenda outro nextTick cria um loop infinito na fila — nenhum timer, I/O ou setImmediate executa.
// ❌ Starvation em produçãofunction loop() { process.nextTick(loop); }loop();// O mesmo vale para queueMicrotask recursivo — mesma armadilha, fila diferente
Use setImmediate para trabalho fatiado — ele cede o loop entre fatias.
Erros em nextTick e queueMicrotask não têm .catch() — vão para uncaughtException
Diferente de Promise.then, onde um erro vira unhandledRejection (capturável com .catch()), erros em nextTick e queueMicrotask disparam uncaughtException diretamente — sem mecanismo de captura por chamada.
// ❌ Sem escape: derruba o processo em Node.js 15+process.nextTick(() => { throw new Error('falha'); });// ✅ Promise.then permite captura localPromise.resolve().then(() => { throw new Error('falha'); }) .catch(err => console.error('capturado:', err.message));
Em Node.js 15+, unhandledRejection também derruba o processo por padrão. Sempre trate erros.
setImmediate não é "imediato" no sentido de microtask — roda por último
setImmediate é uma macrotask da fase check — executa depois de toda drenagem de microtasks. Quem espera que seja “mais imediato” que Promise.then se surpreende.
setImmediate(() => console.log('A: setImmediate'));Promise.resolve().then(() => console.log('B: Promise'));process.nextTick(() => console.log('C: nextTick'));// Saída: C → B → A (nextTick > Promise > setImmediate)
Para executar antes de qualquer fase do loop, use nextTick. Para executar logo após I/O, use setImmediate dentro de um callback de I/O.
Em entrevista
Frase pronta (inglês)
“Node has three microtask APIs with a strict priority order: process.nextTick runs first — it has its own queue that’s drained before any other microtask. Then queueMicrotask and Promise.then are interleaved in the standard microtask queue. Microtasks run between every event loop phase, so they’re higher priority than any timer or I/O callback. The danger of process.nextTick is recursion — a callback that schedules another nextTick will starve the event loop, since the queue is drained completely before phases advance.”
Vocabulário técnico
Português
Inglês
Fila de microtarefas
Microtask queue
Fila de nextTick
nextTick queue / nextTickQueue
Prioridade
Priority
Inanição da fila
Queue starvation
API específica de Node
Node-specific API
Drenagem da fila
Queue draining
Iteração do event loop
Event loop tick / iteration
Avanço de fase
Phase transition
Exceção não capturada
Uncaught exception / uncaughtException
Rejeição não tratada
Unhandled rejection / unhandledRejection
Perguntas frequentes em entrevista
“Qual a diferença entre process.nextTick e setImmediate?”nextTick roda antes de qualquer fase do event loop avançar — é uma microtask com prioridade máxima. setImmediate roda na fase check, que é uma macrotask executada depois de I/O. Em termos de ordem: nextTick → microtasks → I/O → setImmediate → timers (próxima iteração).
“Quando devo usar queueMicrotask em vez de Promise.resolve().then()?”
Quando não há uma Promise natural no contexto e você quer agendar uma microtask sem criar um wrapper de Promise desnecessário. queueMicrotask é mais explícito na intenção e tem overhead ligeiramente menor. Semanticamente são equivalentes em termos de prioridade e ordem. Prefira queueMicrotask em código que precisa ser portável entre Node.js e browser.
“O que acontece se eu lançar um erro dentro de process.nextTick?”
O erro propaga como uncaughtException — não existe .catch() para nextTick. O processo pode ser derrubado se não houver um handler process.on('uncaughtException', ...). Diferente de Promise, onde o erro vira unhandledRejection e pode ser capturado com .catch().
“Um nextTick registrado dentro de uma Promise roda antes ou depois das outras Promises na fila?”
Antes. A nextTickQueue é verificada após cada microtask individual — não apenas ao final de toda a fila. Um nextTick registrado dentro de .then() drena antes das demais Promises já enfileiradas.
O que vem a seguir
Você agora conhece a fila de maior prioridade do event loop. O próximo nível é entender a camada de macrotasks — os timers e setImmediate que executam depois das microtasks, e os detalhes que determinam sua ordem e precisão: jitter de setTimeout, coalescing de setInterval, e a distinção entre a fase check e a fase timers.