Course outline · 0% complete

0/27 lessons0%

Course overview →

The call stack and the task queue

lesson 4-1 · ~12 min · 9/27

It looks at rex's prototype, then that object's prototype, and so on until the chain ends.

Property lookup walks the prototype chain, and the chain is fixed by how the object was created.

Chains are today's theme too, with a different subject. This lesson is about the chain of work, meaning how JavaScript decides what code runs next.

The parallel is loose but useful. Both are ordered searches with a definite rule, and both produce surprising results only when you guess instead of applying the rule.

The difference is that a prototype chain is a shape in memory, while the order of work is a shape in time. Reading it wrong is what makes asynchronous code feel random.

One thread, one stack, one queue

JavaScript runs one thing at a time.

The currently-running functions live on the call stack. Calling a function pushes it on, and returning pops it off.

So consider what setTimeout(fn, 0) actually does. It does not run fn. It registers a timer with the environment, meaning the browser or Node, and returns immediately.

When the timer expires, the environment places fn in the task queue, which is a first-in-first-out list of callbacks that are ready to run and have not started.

The event loop is the rule that connects them. When the call stack is empty, take the oldest task from the queue and run it.

Your currently-running code always finishes first, which is why a 0 ms timeout still runs after every line of your script.

Notice where the timer lives. The stack, the queue, and the loop are the language's concern, and the timer itself is the environment's, which is why setTimeout is not in the JavaScript specification at all.

This single-threaded design explains a lot of real behavior. A slow loop freezes a browser tab, one CPU-hogging request stalls a Node server for every client, and setTimeout(fn, 0) does not mean now.

Every async bug you will ever debug, and every "what does this print" interview puzzle, is decided by this machinery.

A zero-millisecond timeout runs last

The delay is 0 ms and the callback still goes last.

console.log("start");

setTimeout(() => {
  console.log("timeout");
}, 0);

console.log("end");

Output

start
end
timeout

The callback sits in the queue until the whole script has finished and the stack is empty.

The setTimeout call itself is synchronous and fast. It registers the timer and returns on the same line, which is why "end" prints without any pause.

Changing the delay to 500 makes no difference to the order here, since the script finishes in well under a millisecond either way.

Browsers clamp nested timeouts to about 4 ms, so a chain of setTimeout(fn, 0) calls cannot spin faster than roughly 250 times a second. That is a real constraint on animation code written this way.

The useful idiom hidden here is deferral. Wrapping work in setTimeout(fn, 0) says run this after the current script settles, which is how code yields control back to the browser.

call stackmain()task queuetimeout cbevent loop: stack empty? run the next task
setTimeout parks its callback in the task queue. The event loop moves it to the stack only once the stack is empty.

Nothing interrupts the stack

A ready timer cannot force its way in.

The event loop only moves a queued task onto the stack when the stack is empty. There is no preemption, no time slicing, and no priority high enough to break in.

So a long-running loop blocks every timer, click handler, and network callback until it finishes. The work is not lost, it is simply queued behind you.

That is literally what a frozen tab is. The stack never empties, so no queued work ever runs, and the browser cannot even repaint.

The queue keeps filling in the meantime, which is why a page unfreezes with a burst of activity. Every click you made during the freeze is still waiting.

The real fix is to stop occupying the stack. Break the work into chunks that yield between pieces, or move it to a Web Worker, which is a genuinely separate thread with its own stack.

A busy loop blocking a ready timer

The timer becomes ready almost immediately and still waits.

setTimeout(() => console.log("timer fired"), 0);

const start = Date.now();
while (Date.now() - start < 50) {
  // busy-wait: the stack is never empty during these 50 ms
}
console.log("loop finished");

Output

loop finished
timer fired

The callback can only run after loop finished prints and the stack empties.

The timer was ready at roughly 0 ms, so it spent about 50 ms sitting in the queue. setTimeout's delay is the time until the callback is queued, not until it runs.

A busy-wait like this is the wrong way to pause in real code, and it is the clearest way to demonstrate blocking. There is no way to sleep without blocking in synchronous JavaScript, which is exactly why promises exist.

Everyday code blocks the same way without looking like it. A large JSON.parse, a sort over a hundred thousand rows, or a synchronous file read in Node all hold the stack.

If two timers were pending here, both would be queued by the time the loop ended, and they would run in queue order back to back.

It runs after about 3 seconds, once the loop finishes and the stack is empty.

The timer being ready at 100 ms only means fn enters the queue at 100 ms. Entering the queue and running are two different events.

Nothing can interrupt the running stack, so fn waits until the 3-second loop completes.

setTimeout's delay is a minimum, not a guarantee, and that is the sentence to remember. The environment promises not to run the callback early, and it promises nothing about how late.

The same reasoning applies to intervals, with an extra consequence. A 100 ms setInterval during that loop does not queue thirty callbacks, since browsers drop repeats rather than stacking them up.

Measuring elapsed time inside the callback is the honest way to see this. Recording Date.now() before the timer and again inside it reports roughly 3000, not 100.

Ordering three logs

Two synchronous lines and one queued callback.

console.log("a");
setTimeout(() => console.log("b"), 0);
console.log("c");

Output

a
c
b

The order is a c b.

a and c print first because they are synchronous code, running on the call stack as the script executes top to bottom.

The 0 ms timeout only queues b's callback, and the event loop runs it after the script finishes.

Moving the setTimeout line to the top of the file changes nothing, which is the test that proves the point. Source position does not decide order, the stack-then-queue rule does.

The general shape to carry into interviews is a two-pass read. First list every synchronous line in order, then list the queued callbacks in the order they were queued.

That gives the right answer for every puzzle in this unit except one wrinkle, which is that promises get their own higher-priority queue. The next lesson adds it.