Under the Hood
Eventloop

The call stack and run-to-completion

The event-loop lesson leaned on "the call stack" and "the stack is empty" as if they were obvious — this lesson zooms into a single task and shows exactly how frames push and pop, what an empty stack precisely signals to the loop, what a stack overflow actually is, and why the depth and duration of one task is the whole ballgame for responsiveness.

The call stack and run-to-completion

The event-loop lesson built the whole model on top of one sentence: "when the call stack is empty, take the next task from the queue." That sentence is doing a lot of work, and it deserves to be taken apart. What actually is the call stack, mechanically? What does "empty" mean in terms of real memory? And why does a single function — one task, one frame, one long-running loop — have the power to freeze an entire page?

This lesson stays inside a single task. No queues, no timers, no promises — just one function call, and then another, and what the engine does to keep track of where it is.

A stack frame is a receipt for a function call

Every time your code calls a function, the JavaScript engine doesn't just "go run it" and forget where it came from. It pushes a stack frame — a small record that holds that call's parameters, its local variables, and — critically — the address to return to once the function finishes. Think of it as a receipt: it proves you were in the middle of something, and it tells the engine exactly where to resume when that something is done.

Call a function, push a frame. Return from a function, pop the frame — its receipt is torn up, its locals are gone, and execution resumes exactly where the caller left off, reading the return address off the popped frame.

function third() {
  return 'done';
}
function second() {
  return third();
}
function first() {
  return second();
}
first();

Calling first() pushes a frame for first. Inside it, calling second() pushes a frame for second on top of first's frame — first isn't finished, it's paused mid-call, waiting on second's answer. Same again for third. At the deepest point there are three frames stacked: third on top, second below it, first at the bottom. third returns first (last in, first out), its frame pops, second resumes and immediately returns too, its frame pops, first resumes and returns, its frame pops. Three pushes, then three pops, and the stack is empty again.

That's the entire mechanism. Nested calls stack frames deeper; recursive calls do exactly the same thing, a function pushing another frame for itself each time it calls itself, with each frame holding its own copy of the parameters and locals for that particular call.

"The stack is empty" is the loop's cue

Here is why this matters beyond bookkeeping. The event loop's rule was: when the call stack is empty, take the next task from the queue. Now you can read that literally. "Empty" doesn't mean "the program is idle" or "nothing is scheduled" — it means, precisely, that every frame that was pushed for the current task has since been popped. The last frame popping is the signal. There's no separate flag the engine checks; an empty stack is a physical fact about a data structure, and the loop's next move is conditioned directly on that fact.

This is also the exact moment microtasks get their chance to run — the checkpoint that drains the microtask queue fires precisely when the stack empties, before the loop is even allowed to consider the next task. "Stack empty" is the single doorway every subsequent turn of the loop walks through.

Put differently: a task, in event-loop terms, is exactly "push some frames, run them, pop them all." The task queue holds entry points — the outermost function to call next — and running a task means pushing its frame, letting whatever it calls push and pop beneath it, and reporting back to the loop only once that outer frame itself pops and the stack reads empty.

Synchronous execution: busy the whole way through

While there is at least one frame on the stack, the thread is not free. It is not "mostly done" or "available for a quick interruption" — it is executing, frame by frame, with the CPU's attention fully committed. This is what synchronous execution means mechanically: frames pushing and popping in strict order, and the single thread busy for every instant between the first push and the last pop.

Nothing else can happen while that's true, because there is nothing else to happen — one thread, one stack, one thing running. Which leads directly to blocking.

Blocking is just "the stack won't go empty"

"Blocking" sounds like a special condition, but restated in stack terms it is completely ordinary: a task takes a long time, which means one frame (or a deep chain of frames) sits on the stack for a long time, which means the stack does not empty, which means the loop's one and only trigger for moving on — an empty stack — never fires. The loop isn't broken and it isn't waiting on anything mysterious; it is doing exactly what it always does, sitting at "is the stack empty yet?" and getting "no" for as long as your function keeps running.

function blockFor(ms) {
  const start = performance.now();
  while (performance.now() - start < ms) {
    // one frame, on the stack, the entire time — 500ms of "no" to the loop
  }
}

console.log('before');
blockFor(500); // the stack holds this single frame for 500ms
console.log('after');
// No click is handled, no timer fires, and — tying back to the frame-budget
// lesson — no frame is rendered, for the full 500ms this frame sits there.

This is the same freeze the event-loop lesson showed you, and the same one the frame-budget lesson measures in milliseconds against vsync — now you can see exactly where it lives: one frame, refusing to pop, for half a second.

Stack overflow: the depth limit is a memory limit

Frames aren't free. Each one occupies real memory — space for its parameters, its locals, its return address — and the engine reserves a fixed, finite region for the stack (typically on the order of a few megabytes, though the exact figure varies by engine and platform). Push frames fast enough, deep enough, and you run out of that region before you run out of things to call.

That is a stack overflow, and it is not an obscure edge case — it's the direct, physical consequence of pushing more frames than the reserved space can hold. Uncontrolled recursion is the classic way to trigger it: a function that calls itself without ever reaching a base case pushes a new frame on every call and never pops any of them, so the stack grows without bound until it hits the wall.

function countUp(n) {
  return countUp(n + 1); // no base case — never stops pushing frames
}
countUp(0);
// RangeError: Maximum call stack size exceeded

The engine doesn't let this run forever and quietly exhaust the machine's memory; it throws RangeError: Maximum call stack size exceeded the instant the reserved stack region fills up. The limit exists precisely so that a runaway recursion fails loudly and immediately, instead of the process silently consuming more and more memory until something else on the machine breaks.

A stack trace is a snapshot, not a story

When an error is thrown, the engine hands you a stack trace — a printed list of the frames that were on the stack at that instant. Read it top to bottom and you're reading it in the order "where I am" → "who called me" → "who called them," all the way down to wherever execution originally started. The top line is the frame that was actually executing when things went wrong; every line below it is a frame that's still paused, waiting for the one above it to return.

A stack trace from the recursion above would print countUp dozens of times in a row — a direct, readable printout of exactly how deep the pushing had gotten before the limit hit. That repetition isn't noise; it's the stack's own record of what it was doing, visible because the trace is nothing more than a snapshot of frames that genuinely existed, in the genuine order they were pushed.

Depth and duration are the whole ballgame

Zoom back out to the event loop, and a task's effect on responsiveness comes down to exactly two properties of its stack: how long it takes to empty (duration) and, separately, how deep it gets before it does (depth, mattering mainly because it can trigger an overflow rather than because depth alone slows anything down). Everything else about a task — what it computes, what it calls, what data it touches — is invisible to the loop. The loop only ever asks one question, over and over: is the stack empty yet? A task is, from the loop's point of view, "one turn," and for the entire span between its first push and its last pop, nothing else in the program runs. No other click, no other timer, no rendering. That's why a single unbounded loop or a single heavy computation is never "just" slow — it is, for its whole duration, the only thing your one thread is allowed to do.

Where this goes next

We've now looked closely at what happens inside one task. The next lesson looks at what's waiting outside it: Tasks and the task queue covers exactly what timers, DOM events, and I/O completions actually enqueue, why a setTimeout delay is a minimum and never a guarantee, and the surprising 4ms clamp that kicks in once timers nest deep enough.

Go deeper

Check yourself

Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.

  1. What three things does a single stack frame hold, and what event causes a frame to be pushed versus popped?
  2. Walk through the frames pushed and popped for a three-level nested function call, in order.
  3. Precisely, what physical fact about the call stack does 'the stack is empty' refer to, and why is that exactly the event loop's cue to act?
  4. Explain 'blocking' entirely in terms of the call stack, without using the word 'freeze.'
  5. What causes a stack overflow, why does the engine impose a stack size limit instead of letting the stack grow indefinitely, and what error does it throw?
  6. What does a stack trace actually show you, and in what order — top to bottom — does it read?
  7. Why does the lesson say both the depth and the duration of a single task matter for responsiveness, and which one is more directly tied to a stack overflow versus a frozen page?