awesome-everything RU
↑ Back to the climb

Base CS from zero

The event loop

Crux The event loop is a queue of pending callbacks plus a loop that, whenever the call stack is empty, takes the next callback off the queue and runs it. It is why synchronous code finishes before any callback runs.
◷ 24 min

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.

Goal

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.

The idea

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.

The code
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
setTimeout(fn, 0) queues fn. The synchronous lines 1 and 3 run first; the callback's console.log(2) runs only after the stack is empty. Output: 1, 3, 2.
Step-by-step trace

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.

Practice 0 / 5

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?

Check yourself
Quiz

What is the event loop, and what rule governs when a callback runs?

Recap

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.

Continue the climb ↑Concurrency vs parallelism
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.