skip to content

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