Lesson 02: Where the callback waits
Lesson objectives:
- Name the three moving parts: the call stack, the task queue, and the event loop.
- Explain that a
setTimeoutcallback waits in the task queue until it is picked up.- Explain that the event loop moves a queued callback onto the stack only when the stack is empty.
Prerequisites: Lesson 01 (the call stack and run-to-completion) | Previous << 01 | Next 03 >>
"Later" is not a place — so where does the callback actually go?
In Lesson 01 you saw a deferred callback run last. But "deferred" is not an explanation; it is a hand-wave. Where does the callback physically wait between the moment setTimeout registers it and the moment it runs? And who wakes it up? Without answers, you are still trusting a black box. This lesson opens the box. There are exactly three parts, and once you can name all three, "it runs later" turns into "it goes here, and this thing pulls it out when that condition is met."
Explanation
You already know the first part: the call stack, where synchronous code runs, one frame at a time1. The second part is the task queue (you will also hear it called the callback queue or macrotask queue). When a setTimeout delay elapses, its callback does not jump onto the stack — it is placed at the back of the task queue and waits its turn1. A queue is first-in-first-out: callbacks are pulled in the order they were added1.
The third part is the event loop. Its job is almost embarrassingly simple: it watches the call stack, and whenever the stack is empty, it takes the oldest waiting task from the task queue and pushes it onto the stack to run1. That is the whole loop — a job is considered complete only when the stack is empty, and only then is the next task pulled1.
Now Lesson 01's mystery has a mechanism. A setTimeout(fn, 0) callback cannot run while synchronous code is still on the stack, because the event loop only reaches into the task queue when the stack is empty2. Run-to-completion is not a special rule bolted on — it falls out of "the event loop waits for an empty stack."
Read the diagram as a cycle. Synchronous code runs on the stack; along the way, setTimeout drops a callback into the task queue, where it waits (the dotted line). When the script finishes and the stack goes empty, the event loop takes the oldest waiting task and runs it on the stack. If that callback schedules another timer, the cycle repeats. The dotted line is the key: the callback sits idle in the queue the whole time synchronous code is running.
One more piece of vocabulary you will meet: the browser and Node describe this formally with an event loop that has one or more task queues3. You do not need the spec to reason about ordering, but it helps to know that "task queue" is the real, named thing your setTimeout callback lands in — not a metaphor.
Worked example (follow along)
You register two timers and do synchronous work between them:
Trace it through the three parts. The synchronous lines run first on the stack: A, then C, then E print, in that order. Along the way, the two setTimeout lines each drop a callback into the task queue — B's callback is queued before D's. When the script finishes, the stack is empty, so the event loop starts pulling from the task queue in first-in-first-out order: B first, then D. The output is A, C, E, B, D. The three synchronous logs come out in source order because they run on the stack; the two deferred logs come out in registration order because the queue is first-in-first-out.
Your turn (faded example)
Predict the output, thinking in terms of stack versus queue. Fill in the order:
Answer: the order is two, four, one, three. The two synchronous console.log calls run on the stack first, so two and four print in source order. Both setTimeout callbacks wait in the task queue while that happens; the event loop only reaches them once the stack is empty. Then it pulls them first-in-first-out: one was registered before three, so one prints before three. The setTimeout on the very first line still prints among the last, because being first in the source does not mean first onto the stack — it means first into the queue.
Summary + what's next
Three parts run the show: the call stack (synchronous code, one frame at a time), the task queue (where deferred callbacks like setTimeout's wait), and the event loop (which moves a queued task onto the stack only when the stack is empty). "It runs later" now means "it waits in the task queue until the event loop pulls it." But there is a second, higher-priority waiting room we have not mentioned — the one promises use. It is why a .then() you wrote after a setTimeout(fn, 0) still runs before it. That is the microtask queue, and it is next.
Footnotes
-
MDN: JavaScript execution model — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Execution_model ↩ ↩2 ↩3 ↩4 ↩5
-
MDN: setTimeout() — https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout ↩
-
HTML Living Standard: Event loops — https://html.spec.whatwg.org/multipage/webappapis.html#event-loops ↩
Exercises
In a file queue.mjs (or the browser console), paste the five-line A / B / C / D / E worked example. Predict the order first, then run it with node queue.mjs and compare.
Make one timer schedule another: inside the first setTimeout callback, add setTimeout(() => console.log("nested"), 0). Predict where nested lands relative to a second top-level timer, then verify.