Explain the Node.js event loop and its phases.
Quick Answer
The event loop is libuv's mechanism for scheduling callbacks across repeated phases: timers (setTimeout/setInterval), pending callbacks, poll (I/O), check (setImmediate), and close callbacks. Between every phase Node drains the microtask queues (process.nextTick, then Promises).
Detailed Answer
Answer: The event loop is what lets a single-threaded runtime handle many operations concurrently. It runs in repeated iterations ("ticks"), each moving through ordered phases, running the callbacks queued for that phase.
Phases (in order):
- Timers — callbacks scheduled by
setTimeout/setIntervalwhose time has elapsed. - Pending callbacks — certain deferred system callbacks (e.g., some TCP errors).
- Idle/prepare — internal use.
- Poll — retrieve new I/O events and run their callbacks (reading files, sockets, etc.). If nothing is pending, it may block here waiting for I/O.
- Check —
setImmediatecallbacks run here. - Close callbacks — e.g.,
socket.on('close', ...).
Between phases: microtasks. After each phase (and after each macrotask callback), Node drains:
- the
process.nextTickqueue, then - the Promise microtask queue. Microtasks always run before the loop proceeds to the next phase.
console.log('start');
setTimeout(() => console.log('timeout'), 0); // timers phase
setImmediate(() => console.log('immediate')); // check phase
Promise.resolve().then(() => console.log('promise')); // microtask
process.nextTick(() => console.log('nextTick')); // runs before promises
console.log('end');
// start, end, nextTick, promise, timeout, immediate
Key mental model: run synchronous code → drain microtasks → run one phase's callbacks → drain microtasks → next phase. Long synchronous work or a flood of nextTick callbacks can starve I/O.