logodev atlas
4 min read

Node.js — Complete Order of Execution

The Complete Priority Chain

Priority (highest → lowest):
1. Synchronous code (call stack)
2. process.nextTick callbacks        ← Node.js nextTick queue
3. Promise microtasks                ← Promise.then/catch/finally, async/await
4. setImmediate callbacks            ← Check phase
5. setTimeout / setInterval          ← Timers phase
6. I/O callbacks                     ← Pending callbacks / Poll phase
7. close callbacks                   ← Close phase

Critical rule: Steps 2 and 3 run between EVERY event loop phase, draining completely each time.


Code Tracing Examples

Predicting execution order is a core Node.js interview skill. The mental model is: synchronous code empties the call stack first; then the two microtask queues (nextTick, then Promises) drain completely; then the event loop runs one phase at a time, draining its microtask queues after every callback. Working through these examples from the complete priority chain above will make most real-world ordering questions predictable.

Example 1: Basic Order

javascriptconsole.log('1-sync');

setTimeout(() => console.log('2-setTimeout'), 0);

Promise.resolve().then(() => console.log('3-promise'));

process.nextTick(() => console.log('4-nextTick'));

setImmediate(() => console.log('5-setImmediate'));

console.log('6-sync');

// Output:
// 1-sync
// 6-sync
// 4-nextTick
// 3-promise
// 5-setImmediate (or 2-setTimeout — non-deterministic outside I/O)
// 2-setTimeout   (or 5-setImmediate)

Example 2: Inside I/O — Deterministic

javascriptconst fs = require('fs');

fs.readFile(__filename, () => {
  console.log('A-io-callback');

  process.nextTick(() => console.log('B-nextTick'));
  Promise.resolve().then(() => console.log('C-promise'));
  setImmediate(() => console.log('D-setImmediate'));
  setTimeout(() => console.log('E-setTimeout'), 0);
});

// Output (always, inside I/O):
// A-io-callback
// B-nextTick
// C-promise
// D-setImmediate   ← check phase is NEXT after poll
// E-setTimeout     ← timers phase is AFTER check

Example 3: Nested nextTick and Promises

javascriptprocess.nextTick(() => {
  console.log('A');
  process.nextTick(() => console.log('B')); // adds to current nextTick queue
  Promise.resolve().then(() => console.log('C'));
});

Promise.resolve().then(() => {
  console.log('D');
  process.nextTick(() => console.log('E')); // nextTick queue for after THIS microtask
  Promise.resolve().then(() => console.log('F'));
});

// Output: A B D E C F
// Why:
// - nextTick drains: A (adds B to queue, C to promise queue)
// - Continue draining nextTick: B
// - nextTick empty → drain promises: D (adds E to nextTick, F to promise)
// - Promise queue → process nextTick before more promises: E
// - Back to promise queue: C, F

Wait — actually process.nextTick added during promise execution goes to... let me think:

In Node.js, after each microtask (each individual promise resolution), the nextTick queue is checked again BEFORE continuing with remaining promise microtasks.

Corrected output: A B D E C F

Example 4: setImmediate Recursion vs I/O

javascriptconst fs = require('fs');

setImmediate(() => console.log('outer setImmediate'));

fs.readFile(__filename, () => {
  console.log('file read');
  setImmediate(() => console.log('inner setImmediate')); // added during poll phase
});

// Output:
// outer setImmediate  ← runs in check phase (first loop)
// file read           ← I/O callback
// inner setImmediate  ← check phase (second loop)

The Full Sequence Diagram

START:
  Run synchronous code (script.js or REPL input)

LOOP BEGIN:
  ├── Drain process.nextTick queue (ALL)
  ├── Drain Promise microtask queue (ALL)
  │
  ├─ TIMERS PHASE:
  │   Run setTimeout/setInterval callbacks whose time has come
  │   After each callback: drain nextTick + Promise queues
  │
  ├─ PENDING CALLBACKS PHASE:
  │   Run deferred I/O callbacks
  │   After each callback: drain nextTick + Promise queues
  │
  ├─ (IDLE/PREPARE — internal)
  │
  ├─ POLL PHASE:
  │   Execute I/O callbacks in poll queue (drain it)
  │   If empty and setImmediate exists → go to CHECK
  │   If empty and no setImmediate → wait for I/O (up to timer threshold)
  │   After each callback: drain nextTick + Promise queues
  │
  ├─ CHECK PHASE:
  │   Run all setImmediate callbacks
  │   After each callback: drain nextTick + Promise queues
  │
  ├─ CLOSE CALLBACKS PHASE:
  │   Run close event handlers
  │
  └─ If any pending work → LOOP AGAIN
     If nothing pending → EXIT

Key Interview Scenarios

These three scenarios are the most commonly tested in Node.js interviews. Each one isolates a specific rule: the non-determinism of timers at the top level, the absolute priority of nextTick over Promises, and the deterministic behavior of setImmediate when scheduled from inside an I/O callback. Memorizing the rule is less useful than understanding why each one holds.

Scenario 1: Which fires first — setTimeout(0) or setImmediate?

javascript// Outside I/O → NON-DETERMINISTIC
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// Could be either order!

// Inside I/O → ALWAYS setImmediate first
fs.readFile('file', () => {
  setTimeout(() => console.log('timeout'), 0);    // timers phase (later)
  setImmediate(() => console.log('immediate'));   // check phase (next!)
});
// Always: immediate → timeout

Scenario 2: process.nextTick vs Promise

javascriptPromise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
// Always: nextTick → promise
// nextTick queue drains BEFORE Promise microtask queue

Scenario 3: Nested Timers

javascriptsetTimeout(() => {
  console.log('outer timeout');
  setTimeout(() => console.log('inner timeout'), 0);
  setImmediate(() => console.log('inner immediate'));
}, 0);

// Output:
// outer timeout
// inner immediate  ← check phase (comes after current poll phase)
// inner timeout    ← timers phase (next loop iteration)

Interview Questions

Q: What is the complete order of async operations in Node.js? A: Synchronous code → nextTick queue → Promise microtasks → (repeat between phases) → Timers (setTimeout/setInterval) → Pending callbacks → Poll (I/O) → Check (setImmediate) → Close callbacks → back to top.

Q: Does nextTick run before or after Promise microtasks? A: process.nextTick runs BEFORE Promise microtasks. Node.js has two queues: the nextTick queue (higher priority) and the Promise microtask queue. Both drain before moving to the next event loop phase.

Q: Why is the setTimeout vs setImmediate order non-deterministic outside I/O? A: It depends on how quickly the event loop reaches the timers phase vs when setTimeout(fn, 0) threshold is considered passed. The Node.js event loop starts, enters timers phase — if the 1ms minimum hasn't elapsed yet, no timer fires; it moves to poll → check → setImmediate fires. If it has elapsed, timer fires first. This is timing-dependent.

[prev·next]