Create your own
Lesson illustration

Predicting Node.js Execution Order Across Timers, I/O, and Microtasks

Hello, and welcome to the first lesson in Production Node.js and TypeScript Foundations. This module builds the runtime instincts needed to write backend code that remains correct under concurrency, load, and operational pressure—not just code that works in a local happy path.

Today’s goal is to predict the observable execution order of Node.js code that combines synchronous statements, timers, I/O callbacks, promises, and process.nextTick(). This matters in debugging and interviews, but it also prevents production bugs such as responding before required initialization completes, starving I/O, or assuming that a “zero-delay” timer runs immediately.

Plan for roughly 40 minutes: a short visual orientation, an official Node.js reading, then several precise tracing examples.


Start with the runtime model

Node.js executes your JavaScript on one main thread by default. A long synchronous loop therefore blocks everything else: no request handler can run, no completed file-read callback can execute, and no timer callback can start until that JavaScript returns control to Node.

Non-blocking work changes the situation. When code starts an asynchronous operation such as fs.readFile, Node can delegate the underlying work to the operating system or libuv facilities. JavaScript continues executing. Once the operation completes, Node makes its callback eligible to run in an appropriate event-loop phase.

The important distinction is:

  • Starting an asynchronous operation happens now, while the current JavaScript is executing.
  • Running its callback happens later, after the current JavaScript callback or top-level script has completed.

A Promise needs one extra distinction. Creating a promise executes its executor synchronously; a .then(...) reaction runs later as a promise microtask once the promise settles. Promises themselves do not necessarily imply I/O.

Node.js Tutorial - 42 - Event Loop

Watch “Node.js Tutorial - 42 - Event Loop” by Codevolution for a compact map of the runtime queues. It is useful for orienting yourself before adding the more precise rules that follow.

Watch the queue map. Focus on where timers, I/O callbacks, setImmediate, close callbacks, process.nextTick, and promise reactions belong. Treat the diagram as a classification tool, not as a promise that every callback has one universal fixed ordering.

The following flowchart provides a similar overview:

A simplified Node.js event-loop flowchart showing timer, pending-callback, poll (I/O), check (`setImmediate`), and close-callback phases, with the `process.nextTick` and promise microtask queues checked between JavaScript callbacks.

There are two layers in the picture:

  1. Event-loop phases, including timers, poll, check, and close callbacks.
  2. High-priority queues checked between JavaScript callbacks, especially the Node-specific process.nextTick queue and the standard promise microtask queue.

That distinction prevents a common error: process.nextTick() is not itself a normal event-loop phase, despite often appearing near phase diagrams.


The rules you can reliably trace

Read the official Node.js explanation now. It captures both the phase model and the two cases that cause the most confusion in interviews: setTimeout(..., 0) versus setImmediate().

The Node.js Event Loop

Read “The Node.js Event Loop” from the official Node.js documentation. It explains why Node can perform non-blocking I/O with one JavaScript thread, then defines the phases that make callback timing understandable.

First read “What is the Event Loop?”, “Event Loop Explained”, and “Phases Overview.” Start with the non-blocking I/O overview, then study the phase diagram and the explanation that each phase has its own FIFO callback queue. Next, in “Phases in Detail,” read the “poll,” “check,” and “timers” subsections. The poll phase is where ordinary I/O callbacks such as file and network events are handled; the check phase runs setImmediate() callbacks. Note the Node 20 runtime detail in the timer-version note: a phase diagram is a useful model, but it is not a substitute for understanding the scheduling context. Finally, read “setImmediate() vs setTimeout().” Study why top-level scheduling is variable, and why I/O scheduling is predictable.

Rule 1: run all synchronous code first

Node does not interrupt a currently running JavaScript function just because a timer expires or I/O completes. The active call stack must unwind first.

console.log('first');

setTimeout(() => {
  console.log('timer');
}, 0);

console.log('last');

The first two observable outputs are always:

first
last

Only afterward can the timer callback run.

A zero-delay timeout does not mean “run immediately.” It means “make this callback eligible no earlier than the timer threshold.” A busy call stack, operating-system scheduling, and other callbacks can delay it further.

Rule 2: drain Node’s priority queues before moving on

After the currently executing JavaScript operation completes, Node gives priority to:

  1. The process.nextTick queue
  2. The promise microtask queue, including .then, .catch, .finally, queueMicrotask, and async/await continuations

A practical starting rule is:

Synchronous code first; then process.nextTick; then promise microtasks; then eligible event-loop phase callbacks.

Consider this CommonJS example:

console.log('sync: start');

process.nextTick(() => {
  console.log('nextTick');
});

Promise.resolve().then(() => {
  console.log('promise');
});

console.log('sync: end');

Its order is deterministic:

sync: start
sync: end
nextTick
promise

process.nextTick() has higher priority than a normal promise microtask. Its name is historically misleading: it runs before Node proceeds to a later event-loop iteration.

For precise reasoning, there is one valuable nuance:

A process.nextTick() scheduled inside a promise callback does not interrupt promise microtasks already being drained. It runs at the next priority-queue checkpoint.

console.log('A');

process.nextTick(() => {
  console.log('B');
  Promise.resolve().then(() => console.log('C'));
});

Promise.resolve().then(() => {
  console.log('D');
  process.nextTick(() => console.log('E'));
});

console.log('F');

Trace it carefully:

MomentWhat runs or is scheduled
Top-level scriptLogs A, schedules the callbacks, then logs F
nextTick drainLogs B, queues promise callback C
Promise-microtask drainLogs D, queues nextTick callback E, then logs C
Next checkpointLogs E

The output is:

A
F
B
D
C
E

This is more accurate than the oversimplified claim that “every nextTick always beats every promise.” The priority is applied at Node’s queue-draining checkpoints.

Rule 3: know the relevant event-loop phases

For day-to-day backend code, these are the phases worth recognizing:

Phase or queueTypical sourceWhat to remember
process.nextTick queueprocess.nextTick(...)Node-specific priority queue; runs before promise microtasks at a checkpoint
Promise microtasks.then, await, queueMicrotaskRun before Node advances to ordinary phase callbacks
TimerssetTimeout, setIntervalRun after their threshold has elapsed, not at an exact time
PollMost I/O callbacksFile, socket, and network-related callbacks are generally handled here
ChecksetImmediate(...)Runs after poll completes
Close callbacksSome resource 'close' eventsA later phase; usually not central to ordinary request code

The diagram lists pending callbacks and idle/prepare too. They are real parts of the runtime, but most application-level execution-order questions center on timers, poll/I/O, check, and the priority queues.

Do not memorize the phase chart as a simplistic “timers always beat I/O” law. Completion timing matters. A file read may not have completed when a timer becomes eligible; a timer may be delayed because JavaScript or I/O callback work kept the process occupied.


Timers, I/O, and setImmediate

setTimeout(callback, 0) and setImmediate(callback) are similar only at a high level: both defer work. Their execution order depends on where they are scheduled.

At top level: do not promise an order

setTimeout(() => {
  console.log('timeout');
}, 0);

setImmediate(() => {
  console.log('immediate');
});

When both calls originate in the main module, do not claim a stable order. Either callback can appear first depending on runtime timing and system conditions.

For an interview response, the strong answer is:

The synchronous script completes first. Both callbacks are deferred. At top level, the relative order of zero-delay setTimeout and setImmediate is not deterministic, so production code must not rely on either order.

That is better than confidently reciting an ordering that happens to occur on your laptop.

Inside an I/O callback: setImmediate comes first

Now place both scheduling calls inside an I/O callback:

// Run as CommonJS, for example: node ordering.cjs
const fs = require('node:fs');

fs.readFile(__filename, () => {
  console.log('I/O callback');

  setTimeout(() => {
    console.log('timeout');
  }, 0);

  setImmediate(() => {
    console.log('immediate');
  });
});

Here the ordering is predictable:

I/O callback
immediate
timeout

The reason is phase context:

  • The file-read callback runs during the poll phase.
  • setImmediate is queued for the check phase immediately after poll.
  • The timeout must wait until a later timers opportunity, after its threshold is reached.

This makes setImmediate appropriate when your intention is specifically “run after this I/O callback and allow the event loop to continue.”

Now include microtasks in the same I/O callback:

const fs = require('node:fs');

fs.readFile(__filename, () => {
  console.log('I/O');

  process.nextTick(() => {
    console.log('nextTick');
  });

  Promise.resolve().then(() => {
    console.log('promise');
  });

  setTimeout(() => {
    console.log('timeout');
  }, 0);

  setImmediate(() => {
    console.log('immediate');
  });
});

For this isolated program, the output is:

I/O
nextTick
promise
immediate
timeout

The crucial transition happens when the I/O callback returns:

  1. Node drains process.nextTick callbacks.
  2. Node drains promise microtasks.
  3. The event loop continues from poll to check, running setImmediate.
  4. The timer callback runs later, once eligible in a timers phase.

This example combines every category in today’s outcome: I/O, Node priority work, promise work, timers, and check-phase callbacks.


A disciplined method for predicting output

When you face an unfamiliar snippet, avoid trying to “feel” the output from source-code order. Use this short tracing process instead.

1. State the environment and assumptions

Ask:

  • Is this a CommonJS entry file or an ES module?
  • Is the Node version relevant?
  • Is the code at top level or inside an I/O callback?
  • Are we being asked for a fully deterministic order, or only the guaranteed prefix?

This matters because ES module evaluation changes some top-level ordering details. In particular, top-level ESM evaluation is itself handled through promise machinery, so the ordering of initial promise reactions and process.nextTick can differ from the CommonJS examples above. For normal backend debugging, first establish the module format rather than assuming CommonJS behavior.

2. Execute the synchronous portion on paper

Mark every immediate console.log, assignment, function call, and promise executor. Add deferred callbacks to a list, but do not run them yet.

3. Label every deferred callback by destination

For example:

APIDestination
process.nextTick(fn)Node priority queue
Promise.resolve().then(fn)Promise microtask queue
setTimeout(fn, 0)Timers
fs.readFile(path, fn)Poll/I/O callback once the read completes
setImmediate(fn)Check phase

4. Drain priority work after each callback

Whenever top-level code, a timer callback, an I/O callback, or an immediate callback finishes, check the queues in this order:

  1. process.nextTick
  2. Promise microtasks

Keep draining each until no currently queued work remains. Remember the nested case: promise callbacks already queued may run before a nextTick scheduled from one of those promise callbacks.

5. Apply only guarantees you actually have

You can confidently assert:

  • Synchronous work precedes deferred callbacks.
  • nextTick precedes promise microtasks at a checkpoint.
  • Both priority queues run before Node advances to ordinary event-loop phase callbacks.
  • setImmediate scheduled inside a poll/I/O callback runs before a zero-delay timeout scheduled in that same callback.

You should not assert:

  • A top-level setTimeout(..., 0) always beats setImmediate, or vice versa.
  • An I/O callback necessarily runs before a timer merely because the I/O call appears first in source.
  • A timer runs precisely at its requested delay.

Why this is a production concern

This is not merely console-output trivia. A few design consequences follow directly from the ordering rules.

Do not recursively schedule unbounded process.nextTick work. Because Node drains that queue before returning to poll, recursive use can prevent I/O, timers, and incoming requests from progressing. This is called event-loop starvation.

Treat timeout values as deadlines for eligibility, not scheduling guarantees. A 100 ms timeout callback may run later because the event loop is busy. Later in the course, this distinction will matter for retries, dependency deadlines, graceful shutdown, and performance diagnosis.

Make order an explicit dependency when it matters. If an operation must happen after another, represent the relationship with await, promise composition, a callback completion, or a defined queue—not with a guess about timer ordering.

For example, this is explicit:

await persistProject(project);
await publishProjectCreated(project.id);

By contrast, starting both operations and hoping a zero-delay timer runs after the database operation is not a correctness strategy.


Key takeaways

Node execution order becomes tractable when you separate four ideas:

  • Synchronous JavaScript runs to completion before deferred callbacks.
  • process.nextTick has priority at Node’s callback checkpoints.
  • Promise reactions and await continuations run as promise microtasks after nextTick work.
  • Timers, I/O callbacks, and setImmediate are scheduled in different event-loop contexts, so their relative order is sometimes guaranteed and sometimes intentionally not deterministic.

In particular, remember the two interview-ready statements:

  • At top level, do not rely on the relative order of setTimeout(..., 0) and setImmediate(...).
  • When both are scheduled inside an I/O callback, setImmediate(...) runs first.

Next, we will build on this runtime model by coordinating multiple asynchronous operations with Promise combinators and cancelling work safely with AbortSignal.

Can't find a good explanation? Sign up and we'll make it for you

Sign up