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:

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:

3. Idle, Prepare Phase

Used internally by Node. You don't interact with this directly.

4. Poll Phase

The most important phase. It:

// 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

Exit Condition

The loop exits when:

// 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

  1. Six phases cycle continuously (timers → poll → check → close)
  2. Poll phase handles most I/O operations
  3. nextTick and Promises run between phases
  4. Never block the event loop with synchronous work
  5. The loop exits when there's nothing left to do