The Event Loop in Depth
The event loop is the heartbeat of Node. It's the mechanism that lets one thread run your synchronous code, hand off I/O, and then pick up each completed operation's callback at the right moment. Almost every confusing "why did this run in that order?" question in Node is answered by understanding the loop. Let's build it from the ground up.
The pieces involved
Piece | Role |
|---|---|
Call stack | Where the currently-running function executes (one at a time) |
Node APIs / libuv | Perform I/O, timers, etc. off the main thread |
Callback queues | Hold callbacks whose operations have finished |
Microtask queue | Holds promise reactions & |
Event loop | Moves ready callbacks onto the empty call stack |
The one rule that explains everything
The big picture
How a callback travels back to your code
┌─────────────┐ run your code, one frame at a time
│ CALL STACK │ ◄───────────────────────────────┐
└─────┬───────┘ │
│ async call (readFile, setTimeout…) │ loop pushes a
▼ │ callback when
┌─────────────┐ OS/libuv do the slow work │ the stack is EMPTY
│ Node / libuv│ ─────────┐ │
└─────────────┘ ▼ when done │
┌──────────────┐ │
│ callback │───────────────┘
│ queues │
└──────────────┘
▲ EVENT LOOP drains microtasks first, then a queued callbackA canonical ordering example
This snippet trips up nearly everyone the first time. Walk through it with the one rule in mind:
console.log('1: sync start')
setTimeout(() => console.log('2: timeout'), 0)
Promise.resolve().then(() => console.log('3: promise'))
console.log('4: sync end')1: sync start 4: sync end 3: promise 2: timeout
Why this order? Both synchronous logs run first (they're on the stack now). Then, before the loop touches any timer, it drains the microtask queue — so the promise callback (3) runs. Only then does the loop enter its timer phase and run the setTimeout callback (2). Microtasks always beat macrotasks.
The loop runs in phases
Each turn ("tick") of the loop isn't one undifferentiated queue — it's a fixed sequence of phases, each with its own callback type. Timers run in one phase, I/O callbacks in another, setImmediate in another:
One iteration of the loop (simplified)
┌─► timers (setTimeout / setInterval callbacks)
│ pending I/O (some system callbacks)
│ poll (retrieve new I/O; run I/O callbacks)
│ check (setImmediate callbacks)
└── close (e.g. 'close' events)
↑ microtasks drained between EACH phaseEach phase is detailed in Event Loop Phases.
Why blocking is catastrophic here
The takeaways
One thread, run-to-completion — each task finishes before the next starts; no preemption, no races.
Callbacks wait for an empty stack — they never interrupt running code.
Microtasks (promises) outrank macrotasks (timers, I/O) — and drain fully between them.
The loop has ordered phases — which is why
setTimeout(…, 0)andsetImmediatecan differ.Keep tasks short — the loop is shared; hog it and everything stalls.