Node.js Event Loop
The event loop is the heart of Node.js. It decides which task runs and when. Understanding it explains why Node.js handles many users with a single main thread.
A Simple Picture
Imagine a chef in a kitchen with a pile of order slips. The chef takes one slip, starts the dish, and moves to the next slip. A timer rings when the oven finishes. The chef plates that dish and continues. The chef never stands idle beside the oven.
Order slips (tasks) ---> [ CHEF = Event Loop ]
|
starts slow jobs in the background
|
oven rings (job done) --> plate the dish (callback)
Main Parts
Call Stack
The call stack holds the function that runs right now. Node.js runs one function at a time. A function leaves the stack when it ends.
Background Workers
Slow jobs such as file reads and network calls run outside the stack. The system or the thread pool handles them.
Callback Queue
A finished job places its callback in a queue. The callback waits there for its turn.
Event Loop
The loop watches the call stack. An empty stack signals the loop to move the next callback from the queue onto the stack.
+-------------+ +---------------------+
| Call Stack | | Background Workers |
| (runs code) |----->| (files, network) |
+-------------+ +---------------------+
^ |
| v
| +-----------------+
+----------------| Callback Queue |
Event Loop +-----------------+
moves callbacks when the stack is empty
Watch It Work
console.log("1: Start");
setTimeout(() => {
console.log("3: Timer finished");
}, 0);
console.log("2: End");The output shows 1, 2, and 3. The timer waits in the queue even with a zero delay. The main code finishes before the loop picks the callback.
Phases of the Loop
The loop runs through phases in a fixed circle. Each phase handles one kind of callback.
+--> 1. Timers (setTimeout, setInterval) | 2. Pending (system errors) | 3. Poll (new input, file and network results) | 4. Check (setImmediate) | 5. Close (closed connections) +----------- repeat ---------+
- Timers run callbacks whose delay has passed.
- Poll collects finished input and output work.
- Check runs setImmediate callbacks.
- Close handles cleanup events.
Microtasks Jump the Queue
Promises and process.nextTick create microtasks. Microtasks run immediately after the current function, before the loop moves to another phase.
console.log("A");
setTimeout(() => console.log("B: timeout"), 0);
setImmediate(() => console.log("C: immediate"));
Promise.resolve().then(() => console.log("D: promise"));
process.nextTick(() => console.log("E: nextTick"));
console.log("F");The typical order is A, F, E, D, B, C. The two timer styles B and C can swap places in the main file. The microtasks E and D always come first.
Priority (high to low): 1. Current code 2. process.nextTick 3. Promise callbacks 4. Timers 5. setImmediate
Blocking the Loop
A long task on the call stack freezes the loop. Every user waits until the task ends.
// Bad: blocks all users for several seconds
function slowTask() {
let total = 0;
for (let i = 0; i < 5e9; i++) total += i;
return total;
}Ways to Avoid Blocking
- Use asynchronous functions for files and networks.
- Split big loops into smaller pieces with setImmediate.
- Move heavy math to worker threads.
- Avoid the Sync versions of functions in servers, such as readFileSync.
Why This Design Works
Most web apps spend time waiting for databases and files. The event loop uses that waiting time to serve other users. One thread handles thousands of connections with little memory.
Practice Tasks
- Predict the output order of a script that mixes setTimeout, a promise, and a plain log, then run it to check.
- Write a loop that blocks the program for two seconds and watch a timer arrive late.
- Replace the blocking loop with chunks that call setImmediate.
