The Event Loop: Overview
The event loop is the heart of Node.js. It's the mechanism that allows Node to perform non-blocking I/O operations despite JavaScript being single-threaded.
Why Do We Need It?
JavaScript can only execute one piece of code at a time (single-threaded). But servers need to:
- Handle thousands of concurrent connections
- Read/write files without blocking
- Make network requests without waiting
- Process timers and intervals
The event loop solves this by offloading operations to the system kernel (when possible) or a thread pool, then queuing callbacks when those operations complete.
The Event Loop Diagram
┌───────────────────────────┐
┌─>│ timers │ setTimeout, setInterval
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │ System operation callbacks
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │ Internal use only
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │ setImmediate
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │ socket.on('close')
└───────────────────────────┘
Phase-by-Phase Breakdown
1. Timers Phase
Executes callbacks scheduled by setTimeout() and setInterval().
setTimeout(() => console.log('Timer 1'), 100);
setTimeout(() => console.log('Timer 2'), 100);
// Both fire in timers phase when 100ms has passed
2. Pending Callbacks Phase
Executes I/O callbacks deferred from previous iteration:
- TCP error callbacks (e.g.,
ECONNREFUSED) - Some system-level callbacks
3. Idle, Prepare Phase
Used internally by Node. You don't interact with this directly.
4. Poll Phase
The most important phase. It:
- Retrieves new I/O events
- Executes I/O related callbacks
- Can block here waiting for events
// Poll phase handles:
fs.readFile('file.txt', (err, data) => {
// This callback runs in poll phase
});
server.on('connection', (socket) => {
// This callback runs in poll phase
});
5. Check Phase
Executes setImmediate() callbacks.
setImmediate(() => {
console.log('Immediate callback');
});
6. Close Callbacks Phase
Executes close event callbacks:
socket.on('close', () => {
console.log('Socket closed');
});
How the Loop Cycles
// Example iteration:
// 1. Enter loop
// 2. Check timers (run any due callbacks)
// 3. Run pending I/O callbacks
// 4. Poll for new I/O events
// 5. Run setImmediate callbacks
// 6. Run close callbacks
// 7. Loop again (or exit if nothing to do)
Key Concepts
Callback Queues
Each phase has its own FIFO (First In, First Out) queue of callbacks.
Execution Rules
- All callbacks in a phase's queue run before moving to next phase
- Exception: maximum callback limit (to prevent starvation)
Exit Condition
The loop exits when:
- No more callbacks scheduled
- No pending operations
- No handles being waited on
// This will exit immediately:
console.log('Hello');
// This will keep running:
setInterval(() => console.log('tick'), 1000);
// This will exit after callback runs:
setTimeout(() => console.log('done'), 1000);
The Two Special Queues
Outside the main phases, Node has two special queues with highest priority:
1. nextTick Queue
process.nextTick() callbacks run between phases.
2. Microtask Queue
Promise callbacks (.then(), .catch(), .finally()).
setTimeout(() => console.log('1'), 0); // Timers
Promise.resolve().then(() => console.log('2')); // Microtasks
process.nextTick(() => console.log('3')); // nextTick
console.log('4'); // Sync
// Output: 4, 3, 2, 1
Blocking the Event Loop
The event loop runs on a single thread. Blocking it blocks everything:
// DON'T DO THIS:
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// This blocks the entire server for seconds:
app.get('/slow', (req, res) => {
const result = fibonacci(45);
res.send({ result });
});
Key Takeaways
- Six phases cycle continuously (timers → poll → check → close)
- Poll phase handles most I/O operations
- nextTick and Promises run between phases
- Never block the event loop with synchronous work
- The loop exits when there's nothing left to do