Event Loopcritical

Event Loop Phases

The event loop cycles through six phases repeatedly: timers, pending callbacks, idle/prepare, poll, check, and close callbacks. Each phase has a FIFO queue of callbacks to execute.

Memory anchor

The event loop is a theme park ride with 6 stations (phases). The cart stops at each station in order. Between every station, VIP passengers (nextTick) and priority riders (Promises) get to cut the line and board first. The poll station is the lazy river—it idles there if nothing else is scheduled.

Expected depth

1. Timers: executes setTimeout and setInterval callbacks whose delay has elapsed. 2. Pending callbacks: executes I/O callbacks deferred to the next iteration. 3. Idle/prepare: internal use. 4. Poll: retrieves new I/O events; blocks here if queue is empty and no timers are imminent. 5. Check: executes setImmediate callbacks. 6. Close callbacks: socket.on('close', ...) etc. Between every phase transition, Node drains the microtask queue (process.nextTick callbacks first, then Promise microtasks).

Deep — senior internals

The poll phase has nuanced behavior: if the poll queue is empty, Node checks if any setImmediate callbacks are scheduled—if yes, it moves to check immediately; otherwise it blocks waiting for I/O (up to the timeout needed by the next timer). This is why setImmediate reliably fires before a setTimeout(fn, 0) when both are scheduled inside an I/O callback: the event loop is already past the timers phase and will hit check before looping back to timers. The exact behavior from the main module is non-deterministic because process startup time affects whether the timer's 1ms minimum has elapsed. Microtask queue draining between phases is a Node.js ≥11 behavior change—in ≤10, microtasks only drained between full loop iterations.

🎤Interview-ready answer

The event loop has six phases in order: timers → pending I/O → idle → poll → check → close. The poll phase is where Node blocks waiting for I/O if nothing else is pending. setImmediate fires in check (after poll), while setTimeout fires in timers (start of loop). Between every phase, Node drains process.nextTick first, then Promise .then() microtasks.

Common trap

Many candidates memorize 'nextTick fires before promises' but miss that both drain between every phase transition, not just once per loop iteration. A deeply recursive nextTick chain starves I/O callbacks indefinitely because nextTick always wins the drain race.

Related concepts