Case 11/13

Prefer reading? The full explanation

How Node Runs It

process.nextTick vs Promise · Node.js · CommonJS

Before this

How Promises Run — the microtask vs task distinction.

After this you can explain

Why the same Node file prints a different order as .cjs and as .mjs, and when process.nextTick is the wrong tool.

console.log('1: sync');

setTimeout(() => console.log('5: timeout'), 0);

Promise.resolve().then(() => console.log('4: promise'));

process.nextTick(() => console.log('3: nextTick'));

console.log('2: sync end');

Output: 1: sync → 2: sync end → 3: nextTick → 4: promise → 5: timeout

Why does nextTick run before the promise?

In a CommonJS file the output is 1 → 2 → 3 → 4 → 5. The process.nextTick callback is registered on line 7, after the promise on line 5, and still runs first.

Node maintains two separate queues that are drained between event loop phases: the nextTick queue first and completely, then the promise microtask queue. Because they are different queues, the order you registered callbacks across them is irrelevant — the same reasoning that makes promises beat timers in the browser, applied one level up.

The same file as ESM prints a different order

Save that snippet as t.cjs and t.mjs and run both. The CommonJS run prints 3: nextTick before 4: promise. The ESM run prints them the other way round.

The reason is that an ES module is evaluated as a module job, which itself runs inside a microtask. Top-level code in a .mjs file is therefore already executing within a microtask checkpoint, so when the body finishes, Node continues draining that checkpoint — running the pending promise — before it returns to the nextTick queue.

This is why a Node case should never be labelled just "Node.js". Any article that states a nextTick ordering without naming the module system is only half right, and this site labels every Node case explicitly.

Where these queues sit in the loop

Node's event loop runs through phases: timers, pending callbacks, poll, check, close callbacks. Between phase transitions it drains the nextTick queue and then the microtask queue.

That is why the setTimeout callback prints last in both runs. It belongs to the timers phase, and both intermediate queues are emptied before the loop advances to it. The CommonJS/ESM difference only reorders the two intermediate queues relative to each other, never the timer.

Why the Node docs call it legacy

Each queue is drained until empty, including callbacks added during the drain. A process.nextTick callback that schedules another one therefore pins the loop: promises never settle, timers never fire, and incoming I/O is never processed. The third experiment shows a two-step version of this.

Worth being precise about, because it is easy to draw the wrong conclusion: starvation is not unique to nextTick. A recursive queueMicrotask() or an endlessly growing promise chain starves the loop in exactly the same way — the microtask queue is also drained until empty. The rendering case on this site shows that version of the problem blocking paint.

So the reason Node recommends queueMicrotask() for ordinary deferral is portability and standard semantics, not safety. process.nextTick() is worth reaching for only when you specifically need its Node-only ordering — running before promise callbacks — such as emitting an error after the current call returns but before control goes back to the loop.

Reading about it is one thing. Watching it run is another.

↑ Run it step by step

In the wild

Where this rule bites real code

The event emitted before anyone could listen

Symptom
A library emits "ready" inside its constructor. Consumers attach .on("ready") on the next line — and sometimes never hear it.
Why
Emitting synchronously fires before the caller's next line has run. This is THE legitimate nextTick use: defer the emit until after the current call returns, but before anything else.
Fix
In library code, wrap post-construction emits in process.nextTick. In application code, prefer queueMicrotask — same "after this turn" timing, portable to the browser.

Environment

Node.js · CommonJS

CI verified

Node.js v22.22.3

executed as both CommonJS and an ES module job

Last reviewed

29 July 2026

The animation on this page is a teaching model, not a diagram of browser internals. Real engines use several task queues, optimise aggressively, and do plenty of work these panels do not show. What is guaranteed is the observable ordering: the stated output is produced by actually executing the code in CI, on the runtime named above. Browser-specific behaviour follows the HTML Standard and is not executed in a real browser by CI. How this is verified →