Node.js Event Loop Explained: Phases, Microtasks, and the Output Questions Interviewers Ask

Predict Node.js output by separating synchronous work, the nextTick and Promise queues, and the timers, poll, and check phases of the event loop.

KnowledgeGate Team

Exam prep & CS education

Updated 6 Aug 20266 min read

"Node is single-threaded but non-blocking" is easy to repeat and easy to misunderstand. Add setTimeout, setImmediate, process.nextTick, and a resolved Promise to one output question, and the slogan stops helping. The reliable method is to separate synchronous work, priority queues, and event-loop phases.

The Node.js event loop in one sentence

Node runs JavaScript on its main thread, hands supported asynchronous work such as timers and I/O to the surrounding runtime and libuv, then executes ready callbacks as the loop visits its phases. The program can keep making progress because the JavaScript thread does not sit and wait for each I/O operation to finish.

This does not mean every task becomes non-blocking. A long CPU loop written in JavaScript still occupies the main thread and delays all callbacks behind it.

The libuv event loop phases

One pass through the loop visits named phases. Libuv also runs an internal idle and prepare step that JavaScript never schedules into, so these five are the ones worth reasoning about:

  1. Timers: callbacks for due setTimeout and setInterval timers.

  2. Pending callbacks: selected system-operation callbacks deferred from an earlier loop pass.

  3. Poll: new I/O events are collected and their callbacks run.

  4. Check: setImmediate callbacks run.

  5. Close callbacks: close events such as a socket's close callback run.

Timers, poll, and check carry most output questions. A timer delay is a threshold, not a promise that the callback runs at that exact millisecond. The callback still waits until JavaScript is free and the loop reaches an eligible timers phase.

nextTick and Promise callbacks jump ahead

Node also has work that is not treated like an ordinary timers or check callback. After the current synchronous work and at callback or phase boundaries, Node drains the process.nextTick queue before the Promise microtask queue. Only then does it continue with event-loop phases.

That gives a practical priority order for a basic top-level snippet:

  1. Synchronous statements.

  2. process.nextTick callbacks.

  3. Promise reactions such as .then().

  4. Ready event-loop callbacks in their eligible phases.

The nextTick queue is Node-specific. A resolved Promise and queueMicrotask use the JavaScript microtask queue, while process.nextTick has the higher-priority Node queue. Microtask ordering is a JavaScript language rule rather than a Node addition, and the Complete JavaScript course builds it up from the call stack.

A fully traced Node.js output question

Trace this exact program:

console.log('1: sync');
setTimeout(() => console.log('2: timeout'), 0);
setImmediate(() => console.log('3: immediate'));
Promise.resolve().then(() => console.log('4: promise'));
process.nextTick(() => console.log('5: nextTick'));
console.log('6: sync end');

Start with the synchronous pass.

  • console.log('1: sync') prints 1: sync.

  • setTimeout registers a timer callback. It does not print yet.

  • setImmediate registers a check-phase callback. It does not print yet.

  • .then() queues a Promise reaction.

  • process.nextTick() queues a nextTick callback.

  • The final console.log prints 6: sync end.

The call stack is now empty. Node drains the nextTick queue first, so it prints 5: nextTick. It then drains the Promise microtask queue, so it prints 4: promise.

The deterministic prefix is therefore:

1: sync
6: sync end
5: nextTick
4: promise

The remaining callbacks are the zero-delay timer and the immediate, and at top level their relative order is not guaranteed. The reason is worth knowing. Node raises a zero delay to one millisecond, so whether the timer is already due when the loop first reaches its timers phase depends on how long process startup took. If that millisecond has elapsed, the timer callback runs first. If it has not, the loop finds nothing due in timers, moves on, and the check phase runs the immediate. Both endings are therefore possible:

2: timeout
3: immediate
3: immediate
2: timeout

An answer that gives the four-line prefix and explicitly leaves the final pair timing-dependent is stronger than one that memorises a single run from one machine.

Ring of the libuv phases timers, pending, poll, check and close, with the traced timeout callback at timers, the immediate at check, and a side panel draining nextTick before Promise.

The queues drain again between callbacks, not just once

The four-line prefix can leave the impression that process.nextTick and Promise callbacks get a single turn at the end of the synchronous pass. They get a turn after every callback the loop runs. Two timers set to the same delay make that visible:

setTimeout(() => {
  console.log('A: first timer');
  process.nextTick(() => console.log('B: nextTick from timer'));
  Promise.resolve().then(() => console.log('C: promise from timer'));
}, 0);

setTimeout(() => console.log('D: second timer'), 0);

Both timers are due in the same timers phase, so a phase-only model predicts A then D, with B and C trailing behind them. That prediction is wrong. Node finishes the first timer callback, drains the nextTick queue, drains the microtask queue, and only then runs the second timer callback that was already waiting in the same phase. This output is deterministic:

A: first timer
B: nextTick from timer
C: promise from timer
D: second timer

That rule settles most of the harder output questions. A callback scheduled from inside a phase callback does not wait for the phase to empty. Read every process.nextTick and every .then() as a jump to the front of the line, taken at the very next callback boundary.

setTimeout versus setImmediate inside I/O

At top level, do not claim that setTimeout(fn, 0) always beats setImmediate(fn). The useful deterministic case is inside an I/O callback:

const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => console.log('timeout'), 0);
  setImmediate(() => console.log('immediate'));
});

The file-read callback runs in the poll context. After poll, the loop proceeds to the check phase, where the immediate is ready. The timer waits for the loop to return to an eligible timers phase. The output inside this callback is therefore:

immediate
timeout

The explanation matters more than the pair itself: poll is followed by check, while the timer belongs to another phase on a later progression of the loop.

Event loop traps worth naming

Recursive nextTick starvation

A nextTick callback can schedule another nextTick callback. Because Node drains that queue before continuing, an unbounded recursive chain can prevent the loop from reaching timers and I/O. High priority is useful for small follow-up work, but dangerous when it keeps refilling itself.

setImmediate does not behave this way, which is what makes it the safer tool for splitting long work into chunks. A check phase runs the immediates that were already queued when it began, so an immediate that schedules another immediate hands control back to the loop and a pending zero-delay timer still fires within a few loop iterations. An unbounded nextTick chain never yields at all.

Blocking JavaScript

If synchronous code performs a large calculation for several seconds, no timer or I/O callback can run on the main JavaScript thread during that time. Offloading I/O does not make CPU-heavy JavaScript disappear.

Treating zero as immediate execution

setTimeout(fn, 0) means the callback becomes eligible after the minimum delay. It still waits for the current stack, priority queues, and an eligible event-loop phase.

Memorising one top-level timer order

A local run is an observation, not a language guarantee. State only the deterministic portion, then explain why the timer-immediate pair can vary.

How interviewers test the event loop

Expect three common forms: predict a print order, find code that starves the loop, or explain why an immediate ran before a zero-delay timer inside file I/O. Write each scheduled callback into a labelled bucket before producing the output. That prevents a Promise callback from being mixed with timers or an immediate from being placed in poll.

Streams, clustering and Express get tested in the same interviews, and Node.js Interview Questions for Freshers works through those alongside the event loop.

The short version and your next step

Run synchronous code first, drain process.nextTick before Promise microtasks, then reason about eligible callbacks by phase. Remember timers, poll, and check. At top level, the timer-immediate pair can vary; inside an I/O callback, the immediate runs first.

About 600 practice questions on the MERN stack are live on KnowledgeGate, with Node sitting inside that track. Build the model end to end in the Complete Node.js, Express.js and MongoDB course, then pair backend depth with coding-round work through the MERN Stack + DSA bundle. If placement rounds are the only pressure right now, the Coding and DSA courses for placements cover that side on their own.