What is the event loop in Node.js, what are its phases, and how does it differ from the browser's event loop?
Show answer — try answering out loud first
The event loop is the mechanism that allows Node.js to run non-blocking operations with a single thread. It cycles through several phases (timers, pending callbacks, poll, check, close callbacks), relying on the libuv library. Between each phase the microtasks (Promises and process.nextTick) are drained. It differs from the browser's event loop mainly in that Node has those libuv-specific phases and the extra functions process.nextTick and setImmediate.
The event loop is an infinite cycle that keeps asking: "are there any callbacks ready to run?". When an I/O operation finishes (for example a file finished being read), its callback becomes "ready" and the event loop runs it in the phase it belongs to.
Node organizes the work into phases, and on each turn of the cycle it goes through them in this order:
- Timers: runs the callbacks of
setTimeoutandsetIntervalwhose time has already elapsed. - Pending callbacks: runs some I/O callbacks that were left pending from the previous turn (for example certain TCP errors).
- Idle / prepare: internal Node use, we don't touch it.
- Poll: this is where the important stuff happens. Node waits for and collects new I/O events (file reads, network connections) and runs their callbacks.
- Check: runs the callbacks of
setImmediate. - Close callbacks: runs close callbacks, like
socket.on("close", ...).
libuv is the C library underneath Node that implements the event loop and the thread pool. It's the one that actually talks to the operating system to do I/O efficiently (using epoll on Linux, kqueue on macOS, etc.). Node itself doesn't reinvent this: it relies on libuv.
Microtasks run BETWEEN phases. In addition to the phases, there are two special queues that get drained at the end of each phase (and at the end of the initial script):
- The
process.nextTickqueue (highest priority). - The Promise microtasks queue (the
.then,catch,finally, and whatever follows anawait).
That is, after Node runs the callbacks of a phase, it completely drains these two queues before moving on to the next phase.
Difference with the browser:
- The browser doesn't have these libuv phases (timers, poll, check...). It has a simpler notion of "tasks (macrotasks)" and "microtasks".
process.nextTickandsetImmediateonly exist in Node, not in the browser.- In the browser, microtasks are drained after each task; in Node they are drained between each phase of the loop (behavior that was aligned starting with Node 11).
// This example shows the order of the phases and the microtasks.
console.log("1: sincrono");
setTimeout(() => console.log("2: setTimeout (fase timers)"), 0);
setImmediate(() => console.log("3: setImmediate (fase check)"));
Promise.resolve().then(() => console.log("4: promise (microtarea)"));
process.nextTick(() => console.log("5: nextTick (maxima prioridad)"));
console.log("6: sincrono");
// Output:
// 1: sincrono
// 6: sincrono
// 5: nextTick (maxima prioridad)
// 4: promise (microtarea)
// Then the event loop enters its phases:
// 2: setTimeout (fase timers) <- can be swapped with 3
// 3: setImmediate (fase check) <- at the top level, the order 2 vs 3 is NOT guaranteed
Important note: at the top level of the module, the relative order between setTimeout(fn, 0) and setImmediate is not guaranteed (it depends on the machine's performance at that instant). What is 100% deterministic is that first all the synchronous code runs, then nextTick, then the Promise microtasks, and only after that does the event loop enter its phases.
Believing that setTimeout(fn, 0) runs the callback "immediately" or "before everything else". No. The 0 is a minimum, not a guarantee of immediacy: the callback must wait for the event loop to reach the timers phase, and it will always run after the synchronous code, process.nextTick, and the pending Promise microtasks.
"The event loop is what allows Node to do non-blocking I/O with a single thread. It's implemented in libuv and cycles through several phases: timers, pending callbacks, poll, check, and close callbacks. In the timers phase the setTimeouts run, in poll the I/O events are collected, and in check setImmediate runs. The important thing is that between each phase Node drains two microtask queues: first the process.nextTick one, which has the highest priority, and then the Promises one. The difference with the browser is that the browser doesn't have these libuv phases or functions like nextTick or setImmediate; it handles a simpler model of tasks and microtasks."
In what order are these lines printed?
setTimeout(() => console.log("A"), 0);
Promise.resolve().then(() => console.log("B"));
process.nextTick(() => console.log("C"));
console.log("D");
See answer
The order is: D, C, B, A.
Dis synchronous, it runs immediately.C(process.nextTick) has the highest priority among the async work; it's drained right when the script finishes.B(Promise microtask) is drained after thenextTickqueue.A(setTimeout) waits for the event loop to enter the timers phase, which happens after all the microtasks have been drained.