What is the Event Loop?
When you run a Node.js program, it starts executing your script top to bottom. Once the initial execution is done, instead of quitting, Node enters the event loop — a continuous cycle that checks for pending work, processes it, and loops again.
This is what makes Node "non-blocking": while waiting for a file to read or a network response, Node can handle other events instead of sitting idle.
Key insight: JavaScript is single-threaded, but Node uses the OS and libuv's thread pool for I/O. The event loop is the glue that brings results back to your JS code.
The loop has 6 distinct phases, each with its own queue of callbacks. Understanding which phase runs what is the key to mastering async code in Node.js.
Microtasks: The Hidden Layer
Watch out: If you recursively call process.nextTick() you can starve the event loop — I/O callbacks will never run because the microtask queue never empties.
Microtasks don't belong to any phase — they run between every phase transition. A resolved Promise callback always runs before the loop can move on.
Interactive Demo
Click any phase to explore what runs there. Step through with the Prev / Next buttons.
- Runs if delay threshold has passed
- setTimeout(fn,0) still waits until this phase
- Does NOT guarantee exact timing
- System-level error callbacks
- e.g. TCP ECONNREFUSED
- Most I/O runs in Poll instead
- Internal bookkeeping only
- Prepares for the Poll phase
- Not accessible from userland JS
- Runs completed I/O callbacks
- Blocks waiting if queue empty + no timers
- Once timers ready, moves to Timers
- setImmediate() runs here
- Fires before setTimeout(fn,0) inside I/O
- Predictable timing after Poll
- socket.on("close") callbacks
- process.on("exit") fires here
- Loop repeats or exits after this
The 6 Phases
Each iteration of the loop moves through these phases in order:
Runs callbacks scheduled by setTimeout() and setInterval() whose delay threshold has been reached.
Handles I/O callbacks deferred from the previous loop tick — mostly system-level errors like TCP socket errors.
deferred callbacksInternal Node.js use only. Prepares state before entering the poll phase. Your code never runs here.
internal onlyRetrieves and executes new I/O events. If the queue is empty and no timers are ready, the loop blocks here waiting.
fs / net / httpRuns setImmediate() callbacks — always fires right after the Poll phase.
Executes close callbacks like socket.on('close') and process.on('exit').
TL;DR
Each loop iteration: 1. Timers → setTimeout / setInterval callbacks 2. Pending I/O → deferred system callbacks 3. Idle/Prepare → internal, skip 4. Poll → I/O events; may block waiting 5. Check → setImmediate callbacks 6. Close → socket.on('close') etc. ⚡ Between EVERY phase: drain microtasks (nextTick, then Promises)
See It in Code
Can you predict the output order before running this snippet?
// What's the output order? console.log('1. script start'); setTimeout(() => console.log('4. setTimeout'), 0); setImmediate(() => console.log('5. setImmediate')); Promise.resolve().then(() => console.log('3. Promise microtask')); process.nextTick(() => console.log('2. nextTick')); console.log('1. script end');
Output: script start → script end → nextTick → Promise → setTimeout → setImmediate. Microtasks always flush before the loop moves on!
setTimeout vs setImmediate vs nextTick
These three are the most commonly confused. Here's a clear comparison:
| Function | Runs in | Priority | Use when |
|---|---|---|---|
| process.nextTick() | Between any two phases | Highest | Defer but stay before any I/O |
| Promise.then() | Microtask queue | High | Standard async/await patterns |
| setImmediate() | Check phase | Medium | After Poll — inside I/O callbacks |
| setTimeout(fn, 0) | Timers phase | Lower | Delay by at least N ms |