Base CS from zero
The event loop
The last lesson left one thing unexplained. A non-blocking call hands over a callback and returns. Milliseconds later the device finishes — and something calls that callback. The lesson called it “the result-delivery mechanism” and moved on. This lesson is that mechanism, named and traced.
It is called the event loop, and it is simpler than its reputation suggests. It is two
things: a queue that holds callbacks waiting to run, and a loop that takes them off
the queue one at a time. Once you have traced it, a famous puzzle dissolves: why a program
that prints 1, then schedules a callback that prints 2, then prints 3, produces the
output 1, 3, 2 — not 1, 2, 3. The 2 is late on purpose, and the event loop
is exactly why.
After this lesson you can define the event loop as a callback queue plus a loop that drains it, state the rule “run a callback only when the call stack is empty”, explain why synchronous code always finishes before any callback runs, and trace a small program to predict its output order.
Two pieces from earlier units are already in place. From Unit 08: the call stack — the stack of frames for the currently running function calls; the program is “running synchronous code” whenever the stack is non-empty. From this unit’s lesson 02: a non-blocking call hands over a callback to be run when its slow work finishes.
The event loop adds one new piece and one rule.
The new piece — the callback queue. A queue is a waiting line: items are added at the back and removed from the front, in first-in-first-out order (the first callback added is the first one run). The callback queue holds callbacks that are ready to run — their slow device has already finished — but have not run yet. When a device finishes, its callback is not run immediately; it is placed at the back of this queue.
The rule — run a callback only when the stack is empty. The event loop is a loop that repeats forever: check whether the call stack is empty; if it is, take the front callback off the queue and run it; if the stack is not empty, do nothing and check again.
Two consequences follow directly. First, synchronous code always finishes first: while any synchronous code is running, the stack is non-empty, so the loop will not start a callback. Only after the stack drains completely can a queued callback begin. Second, a callback runs to completion: once started, it gets its own stack frames and runs fully before the loop takes the next callback — callbacks never interrupt each other.
The program below uses setTimeout(fn, 0) — a non-blocking call that means “put fn on
the callback queue.” Even with a 0 delay, fn cannot run until the synchronous code
finishes and the stack is empty. That is what produces the 1, 3, 2 order.
1
console.log(1); // synchronous — runs now
2
3
setTimeout(() => {
4
console.log(2); // the callback's body — runs later
5
}, 0);
6
7
console.log(3); // synchronous — runs now
- L1 Synchronous call. The stack is non-empty; this runs immediately. Output so far: 1
- L3 setTimeout is a non-blocking call: it hands the callback over and returns at once. It does NOT run the callback.
- L4 This line is inside the callback. It will run only when the event loop later picks the callback up.
- L5 The 0 means 'queue the callback as soon as possible' — but 'soon' still means after the stack empties.
- L7 Synchronous call. Runs right after setTimeout returns — before the callback. Output so far: 1, 3
Step through the program. The cells are the call stack (bottom-to-top, oldest at left). Watch the synchronous lines run with the stack non-empty, the callback get queued, and the event loop run it only once the stack is empty.
1
console.log(1);
2
3
setTimeout(() => {
4
console.log(2);
5
}, 0);
6
7
console.log(3);
Why this works
Why does setTimeout(fn, 0) not run fn right away — the delay is zero? The 0 sets
the timer, not the run. It means “the callback is allowed to be queued immediately.”
But being queued is not being run. The event loop’s one rule still applies: a callback runs
only when the call stack is empty. At the moment setTimeout is called, the script’s frame
is still on the stack and console.log(3) has not run yet. So fn waits in the queue until
the script finishes. 0 is the minimum queueing delay; it can never shortcut the
empty-stack rule.
Common mistake
A common mistake is to read setTimeout(fn, 0) as “run fn now” or “run fn in 0
milliseconds.” It means neither. It means “put fn on the callback queue; it becomes
eligible to run only after the call stack empties.” If the synchronous code after it takes
50 ms, the 0-delay callback waits 50 ms. The delay number is a minimum wait before
queueing, never a guarantee of when the callback runs.
A program runs: console.log(1); setTimeout(() => console.log(2), 0); console.log(3). What is the first number printed?
Same program. What is the last number printed?
The event loop runs a queued callback only when the call stack has how many frames on it?
Three callbacks A, B, C are queued in that order, and the stack is empty. The event loop runs them. Which runs first — type 1 for A, 2 for B, 3 for C?
A program: console.log(10); setTimeout(() => console.log(20), 0); setTimeout(() => console.log(30), 0). The synchronous part prints how many numbers before any callback runs?
What is the event loop, and what rule governs when a callback runs?
The event loop is the mechanism that runs callbacks. It is two parts: a callback
queue — a first-in-first-out waiting line holding callbacks whose slow work has finished
— and a loop that repeats forever, checking one rule: if the call stack is empty, take
the front callback off the queue and run it; otherwise wait. Two facts fall out of that
rule. Synchronous code always finishes first, because while it runs the stack is
non-empty and the loop will not start a callback. And each callback runs to completion
without interruption, because the loop does not take the next one until the current
callback’s frames have all popped. This is why console.log(1); setTimeout(cb, 0); console.log(3) prints 1, 3, 2: the 0-delay callback is only queued fast — it still
waits for the stack to empty, which happens after 1 and 3 have printed.