Introduction
Node runs the JavaScript that is currently being executed first. Code scheduled for later waits until the current code has finished. However, Node has more than one place where scheduled work can wait, so all “later” work does not run in exactly the same order.
Most event-loop confusion starts with an output-prediction question: why did a Promise callback run before a zero-delay timer, or why did setImmediate() sometimes move ahead of setTimeout()?
The useful answer is not simply “asynchronous code runs later.” Node.js has several scheduling mechanisms. The order depends on which mechanism received the callback and where that callback was scheduled.
Every output example in this article was run as CommonJS on Node.js v22.23.1. Module format matters for some edge cases, particularly process.nextTick() ordering during ES-module evaluation.
Start with the current code
JavaScript begins by executing the current script synchronously. The call stack is the record of functions that are currently running. Calling a function adds it to the stack, and returning from it removes it. Scheduling a callback does not interrupt the statement that is already running.
console.log("start");
setTimeout(() => console.log("timer"), 0);
console.log("end");
Predict the output before reading on:
start
end
timer
setTimeout() registers work with the Node.js host and returns. The script continues to "end". A delay of zero is a threshold after which the callback becomes eligible; it is not an exact execution appointment.
One sequence, several places for waiting work
The diagram separates concepts that are often incorrectly grouped into one “callback queue”:
- synchronous JavaScript runs on the current stack;
- Node maintains a special next-tick queue;
- Promise reactions and
queueMicrotask()use the regular microtask queue; and - event-loop phases handle callbacks such as timers, input/output, and
setImmediate().
A callback is a function given to another operation so that it can be called later. For example, the function passed to setTimeout() is a callback. A microtask is a small piece of work that Node runs after the current JavaScript operation finishes, before it moves to later event-loop work.
This is a documented host model, while the outputs below are reproducible demonstrations on the recorded runtime.
Promises and queueMicrotask()
Promise reactions do not run at the exact moment a Promise settles. They are placed in the microtask queue. queueMicrotask() also places its callback in that queue. Node runs these microtasks after the current JavaScript operation finishes and before it continues to later event-loop callbacks.
console.log("start");
Promise.resolve().then(() => console.log("promise"));
queueMicrotask(() => console.log("microtask"));
setTimeout(() => console.log("timer"), 0);
console.log("end");
Verified output on Node.js v22.23.1:
start
end
promise
microtask
timer
The two synchronous logs appear first. The Promise reaction was enqueued before the explicit microtask, so it runs first when the regular microtask queue is drained. The timer belongs to a later event-loop phase.
Microtasks can enqueue more microtasks, and the runtime continues draining them before moving on. That is useful for finishing small pieces of related work, but an unbounded chain can delay timers and I/O.
process.nextTick() is Node-specific
process.nextTick() is not one of the event-loop phases. Node processes its queue after the current operation finishes and before continuing the event loop. In the CommonJS example used here, it also runs before the regular microtask queue:
console.log("start");
Promise.resolve().then(() => console.log("promise"));
queueMicrotask(() => console.log("microtask"));
process.nextTick(() => console.log("nextTick"));
console.log("end");
Verified output:
start
end
nextTick
promise
microtask
This priority is not a reason to use nextTick() for all deferred work. Repeatedly scheduling more next-tick callbacks can prevent Node from reaching I/O work. It is a specialised Node mechanism for work that must happen immediately after the current operation.
There is also a module-format caveat: Node’s official documentation shows that top-level ES-module evaluation already occurs through the Promise microtask machinery, which can change the apparent order between an immediately resolved Promise and process.nextTick(). Recording CommonJS here keeps the example reproducible.
The event loop has stages
Node’s event loop is not one large first-in, first-out queue. It moves through several stages. A simplified view includes these relevant phases:
| Phase or checkpoint | Relevant work |
|---|---|
| Timers | eligible setTimeout() and setInterval() callbacks |
| Pending callbacks | selected system callbacks deferred to a later iteration |
| Poll | receives and processes most input/output events |
| Check | runs setImmediate() callbacks after poll |
| Close callbacks | handles certain close events |
| Next-tick and microtask checkpoints | run around JavaScript operations, outside the simple phase list |
The exact internal timing of these stages depends on the Node.js and libuv versions. The practical lesson is to reason from where the callback was scheduled instead of memorising one circular picture as if it explains every situation.
setImmediate() versus setTimeout(..., 0)
At the top level, the order of a zero-delay timer and setImmediate() is not a reliable guarantee. Inside an input/output callback, the relationship is clearer: after the poll stage, Node enters the check stage for immediates before returning to timers.
const fs = require("node:fs");
fs.readFile(process.execPath, () => {
setTimeout(() => console.log("timer"), 0);
setImmediate(() => console.log("immediate"));
});
Verified output:
immediate
timer
This does not mean setImmediate() is universally “faster.” It means the callbacks were scheduled while Node was handling I/O in poll, and the check phase follows poll.
Completed work is only eligible work
When the operating system or Node’s worker pool completes asynchronous work, its callback becomes eligible for the appropriate processing path. It still cannot interrupt JavaScript that is already running. A long synchronous calculation holds the JavaScript thread, delays microtask checkpoints, and prevents later phase callbacks from running.
That distinction explains why non-blocking I/O does not make CPU-heavy JavaScript automatically parallel. Node can coordinate work outside the main JavaScript stack, but each callback still needs an opportunity to execute on that stack unless another thread or process is used deliberately.
Node.js versus browsers
ECMAScript specifies language-level jobs such as Promise reactions, but the host defines APIs and event-loop integration. Browsers provide tasks tied to rendering, DOM events, timers, and other Web APIs. Node provides libuv phases, setImmediate(), and process.nextTick(). The shared vocabulary helps, but a browser event loop should not be treated as Node’s phase diagram with different labels.
A practical prediction checklist
When reading asynchronous code, ask in this order:
- What prints or returns synchronously before the current stack finishes?
- Which callbacks enter Node’s next-tick queue?
- Which Promise reactions or
queueMicrotask()callbacks enter the regular microtask queue, and in what enqueue order? - Which event-loop phase owns each remaining callback?
- Was
setImmediate()or a timer scheduled at top level, during I/O, or inside another callback? - Could current synchronous work or an expanding microtask/next-tick chain delay everything else?
That checklist is a design reflection from verifying these examples. It is intentionally more precise than the shortcut “sync first, async second.”
Key takeaways
- Current JavaScript runs to completion before a scheduled callback can use the stack.
- Promise reactions and
queueMicrotask()run as microtasks before later event-loop callbacks. - In the verified CommonJS examples, Node drains
process.nextTick()before regular microtasks. - Timers specify minimum thresholds, not exact execution times.
setImmediate()runs in the check phase; when scheduled inside an I/O callback, it precedes a zero-delay timer scheduled beside it.- Node has multiple queues and phases, not one generic FIFO callback queue.