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

Sequence from synchronous execution through next ticks, microtasks, and a later event-loop callback

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:

  1. What prints or returns synchronously before the current stack finishes?
  2. Which callbacks enter Node’s next-tick queue?
  3. Which Promise reactions or queueMicrotask() callbacks enter the regular microtask queue, and in what enqueue order?
  4. Which event-loop phase owns each remaining callback?
  5. Was setImmediate() or a timer scheduled at top level, during I/O, or inside another callback?
  6. 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.

Sources