skip to content

Call Stack and Run-to-Completion

Every job runs on a single call stack and keeps the thread until that stack is empty — nothing preempts it mid-way. Once run-to-completion clicks, the rest of the event loop stops being magic, and interviewers use it to check you can explain why one long loop freezes an entire page.

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

questions

4

JavaScript engines run your code on a single call stack. What gets pushed onto and popped off that stack, and why does that mean two of your functions can never be executing at the same instant?

level: juniorimportance: must knowfreq 78%

answer

  1. one stack per agent
  2. a frame per in-progress call
  3. calls push, returns pop
  4. only the top frame is running
  5. callbacks always begin on an empty stack

basics

~20 s

The call stack holds one frame per function call that has started but not returned — its arguments, its locals, and the point to resume at. Calls push a frame, returns pop it, and because JavaScript has a single stack, only the top frame is ever running.

solid answer

~40 s

The call stack is the engine's record of the calls that have started but not yet finished. Each call pushes a frame holding that call's arguments, its local variables, and where to resume when it returns; each return pops the frame. Because a JavaScript agent has exactly one stack, only the frame on top is actually executing, so two of my functions can never be mid-execution simultaneously and nothing gets slipped in between two of my statements. Asynchronous work does not break this: a `setTimeout` or `fetch` call returns immediately and its frame pops, the host does the waiting outside JavaScript, and the callback later runs as its own job starting from an empty stack. So "single-threaded" here really means one stack — and whatever holds that stack holds the whole runtime.

go deeper

for a junior

Be ready to say that each call adds a frame and each return removes it, and that JavaScript has one such stack, so your functions never run at the same moment.

for a middle

Explain what a frame actually holds — arguments, locals, and the resume point — and trace a small nested-call example, naming exactly which frames are live when the innermost line executes.

for a senior

Show why it matters operationally: whatever holds the stack owns the entire runtime, so one slow synchronous call has runtime-wide latency effects and shows up in a CPU profile as a single unbroken stack.

for a principal

Own the design consequence: with one stack per agent, the amount of synchronous work permitted per job becomes a system-level budget, and any component capable of holding the stack for long needs architectural separation rather than tuning.

## What the call stack is When a function is called, the engine must remember three things: the arguments it was called with, the local state it builds up while running, and where execution should resume once it returns. It packages that into a **frame** (in specification language, an execution context) and places the frame on a stack — a last-in, first-out structure. A call pushes a frame; a return pops it. The stack is a fixed-size memory region the engine manages for you; you never allocate or address it directly, you only observe its shape through debuggers, profilers and error stack traces. ## Push and pop, in strict order ```js function inner() { console.log('inner running'); } function outer() { inner(); console.log('outer resumes'); } outer(); ``` At the moment `console.log('inner running')` executes, three frames are live: `outer` at the bottom, `inner` above it, and `console.log` on top. `console.log` returns and pops; `inner` reaches its end and pops; control resumes inside `outer` exactly where it left off, because that resume point was stored in `outer`'s frame. Nothing else can be interleaved between those pops — the order is fully determined by the call structure. ## One stack per agent ECMAScript models a running program as an **agent**: one thread of execution, one job queue, one call stack. Within an agent, exactly one frame — the topmost — is executing at any instant. There are only two ways a different function starts running. Either the currently running function calls it, in which case its frame goes *above* the caller's and the caller is suspended in the ordinary sense (it is still on the stack, waiting for the callee to return); or the stack drains completely and the runtime picks up the next queued job, which begins on an empty stack. That second path is the important one. Every callback you ever write — a timer callback, a promise reaction, an event handler — starts at the bottom of an empty stack. It never appears halfway up somebody else's stack. This is why you can reason about a function's body as an uninterrupted unit. ## Then how does anything happen "in the background"? A single JavaScript stack does not imply the *host* is single-threaded. When you start a timer or a network request, the JavaScript call that starts it does almost nothing and returns immediately; its frame pops. The waiting, the socket work, the disk read — those are carried out by the host outside the JavaScript stack, quite possibly on other operating-system threads. When the work finishes, the host queues a job, and your callback runs later on a fresh stack. So the parallelism is real, but it is parallelism of *host work*, not of your JavaScript. Only one piece of your code ever holds the stack. ## What follows from it - **A function that never returns owns the runtime.** Its frame stays on the stack, the stack never empties, and no queued job can start. That is the whole mechanism behind a frozen page. - **You never observe half-updated state from a callback.** A callback cannot begin until the current stack is gone, so it can only ever see state as of a completed statement sequence. - **Frames also pop on `throw`.** An exception unwinds frames one by one until it finds one with a matching `catch`. If none exists, the job ends with an empty stack, the error is reported as uncaught, and the runtime moves on to the next job. - **Depth is finite.** Each live call consumes part of a fixed region, so nesting calls deeply enough exhausts it and the engine throws — the everyday sign that frames really are physical, not conceptual. ## Common misreadings The most frequent one is treating the stack as a queue: "my callbacks are sitting on the stack waiting." They are not — queued callbacks live in job or task queues, and the stack holds only calls currently in progress. The second is inferring extra threads from asynchronous syntax: `async` functions, promises and timers all run their JavaScript on the same single stack; nothing about them starts a second interpreter. The third is imagining the engine can pause your function to squeeze in an urgent callback. It cannot — there is no preemption point between your statements, which is exactly why JavaScript code needs no locks around ordinary object mutation.

  • If only one function runs at a time, how can a network request be in flight while my code keeps running?
    The JavaScript call that starts the request returns immediately and its frame pops; the transfer itself is performed by the host, outside the JavaScript stack and possibly on another thread. When it completes, the host queues a job and your callback runs later on a fresh stack. Only JavaScript execution is serialised — not the work the host performs on your behalf.
  • What happens to the stack when a function throws instead of returning?
    The throw unwinds the stack, popping frames one at a time until it reaches a frame with a matching `try`/`catch`. If no frame catches it, the job ends with the stack empty and the error is reported as uncaught. The runtime then continues with the next queued job, so one job's uncaught error does not stop later ones from running.
  • Does a nested call suspend the caller in the same sense that awaiting does?
    No. When `a()` calls `b()`, `a`'s frame stays on the stack underneath `b`'s and resumes the instant `b` returns — the job never ends and nothing else can run in between. Awaiting is different in kind: the function returns, its frame pops, and the remainder runs later as a separate job on an empty stack.

saying these in an interview costs you the question

  • JavaScript is multi-threaded because callbacks can run in parallel
  • setTimeout runs its callback on a background thread
  • The call stack is where queued callbacks wait their turn
  • Async functions keep executing while later code runs
  • The engine pauses a function to run an urgent callback

context

open as a page

In JavaScript, once a function starts running, can a pending timer callback or promise reaction interrupt it partway through? Explain what run-to-completion guarantees and where that guarantee stops.

level: middleimportance: must knowfreq 68%

basics

~20 s

No. A JavaScript job runs until its call stack is empty, and only then does the runtime pick up the next callback, so nothing is preempted between statements. The guarantee covers one uninterrupted synchronous run — an await ends it and starts a new job.

open as a page

In JavaScript, what makes an engine throw the error 'RangeError: Maximum call stack size exceeded', and why does rewriting the same recursive algorithm as a loop remove that limit?

level: middleimportance: should knowfreq 52%

basics

~20 s

Every call in progress needs a stack frame, and the engine's stack region is fixed in size; recursion that nests deeply enough exhausts it and V8 reports that as a RangeError. A loop reuses one frame, so nothing accumulates.

open as a page

In a browser page, a JavaScript click handler runs a four-second synchronous loop (or a JSON.parse of a very large string). Using the call stack, explain why the whole page stops responding until it finishes, and what happens to the clicks the user makes in the meantime.

level: seniorimportance: should knowfreq 56%

basics

~20 s

The handler holds the only call stack for four seconds, and no queued job can start until that stack is empty, so no other handler, timer or promise reaction runs and nothing repaints. Clicks made meanwhile are queued and their handlers fire in a burst afterwards.

open as a page