# Call Stack

## 1. What the Call Stack Is

The call stack is the mechanism JavaScript uses to track **which function is currently running**, and what to return to when it finishes. It's a LIFO (last in, first out) structure: every function call pushes a new **frame** on top; every `return` (or the function ending) pops that frame off.

![Stack frames for first, second, and third stacked LIFO, growing on call and shrinking on return](diagrams/01-stack-frames.svg)

```js
function third() { console.log("in third"); }
function second() { third(); }
function first() { second(); }
first();

// Stack grows:      Stack shrinks (after each return):
// [first]            [first, second, third] -> logs "in third"
// [first, second]     [first, second]         -> third() returns
// [first, second, third] [first]                -> second() returns
//                       []                       -> first() returns
```

## 2. Reading a Stack Trace

When an error is thrown, its `.stack` property (and what you see printed to the console) shows the call stack **at the moment of the throw**, most recent call first:

```js
function level3() { throw new Error("boom"); }
function level2() { level3(); }
function level1() { level2(); }

try {
  level1();
} catch (e) {
  console.log(e.stack);
  // Error: boom
  //   at level3 (...)
  //   at level2 (...)
  //   at level1 (...)
  //   at <top level> (...)
}
```

Reading a stack trace bottom-to-top (in this printed order, top-to-bottom) tells you the exact chain of calls that led to the error — the most valuable piece of information when debugging.

## 3. Stack Overflow

The call stack has a finite size. Recursion without a proper base case (or simply too deep) exhausts it:

```js
function recurse() {
  return recurse(); // no base case — never stops
}
recurse(); // RangeError: Maximum call stack size exceeded
```

This directly connects to the Time and Space Complexity topic: recursive call depth is the space cost of a recursive algorithm, and the call stack is the concrete structure that cost is measured against.

## 4. The Call Stack Is Why JavaScript Is Single-Threaded

Only one call stack exists per JavaScript execution context (per thread). While any frame is on the stack, nothing else can run — this is exactly *why* asynchronous operations must be deferred and later resumed via the event loop, rather than blocking the stack (fully explored in the Event Loop topic).

```js
function longSyncWork() {
  for (let i = 0; i < 1e9; i++) {} // stays on the call stack the entire time
}
longSyncWork(); // NOTHING else — no timers, no events, no rendering — can happen until this returns
```

## 5. Async Functions and the Call Stack

An `async function` or a `.then()` callback is **not** on the call stack while it's "waiting" — the call stack only holds it during the synchronous portions of its execution. This is precisely why `await`ing something doesn't block other code from running (previewed here, covered fully in the Async/Await topic).

```js
async function example() {
  console.log("A: on the stack");
  await new Promise(resolve => setTimeout(resolve, 100)); // stack frame is POPPED here while waiting
  console.log("B: back on the stack after resuming");
}
example();
console.log("C: runs immediately — the stack was freed during the await");
// order: A, C, B
```

## Under the Hood: What's Actually Inside a Stack Frame, and Why the Limit Exists

### A frame is a real, fixed-size chunk of memory

Each pushed frame isn't just an abstract bookkeeping entry — it's a concrete block of memory holding everything needed to resume the caller once the callee returns: the return address (where to jump back to), the function's local variables that weren't promoted to a heap `Context` (see the Scope topic — only *captured* variables go to the heap; everything else lives right here on the stack frame), and space for any temporary values mid-computation.

### Why the stack has a hard limit at all

Unlike the heap (which can keep growing, subject only to total available memory and triggering garbage collection as needed), the call stack is typically allocated as a **single, fixed-size, contiguous region of memory per thread** when that thread starts — commonly a few megabytes by default. This fixed sizing is a deliberate, practical trade-off: stack allocation/deallocation (push/pop) needs to be extremely fast — just adjusting a pointer — and a contiguous fixed region is what makes that possible. A dynamically-growable stack would lose that speed advantage.

```js
// Node: you can raise this limit (within OS limits) via a CLI flag:
// node --stack-size=8000 script.js
```

### Why JavaScript doesn't have tail-call optimization in practice

The ECMAScript spec technically permits **tail-call optimization** (reusing the current frame instead of pushing a new one, when a function's very last action is calling another function) — but most engines, including V8, have never shipped it for standard function calls, due to real-world complications around debugging (stack traces becoming misleading) and compatibility. This means recursive JavaScript code — even when written in a textbook "tail-recursive" style that would be optimized away in languages like Scheme — still consumes one real stack frame per call in practice, and can still overflow. This is *why* the Time and Space Complexity topic's iterative rewrites of recursive functions are a genuinely necessary technique in JS, not just a style preference — the language does not rescue you from deep recursion's stack cost automatically, regardless of how the recursion is written.

## 6. Common Pitfalls

1. **Assuming deep recursion is "free"** — every recursive call consumes real stack space; very deep recursion (commonly tens of thousands of frames, engine-dependent) throws `RangeError`.
2. **Misreading stack trace order** — the topmost line is the innermost (most recent) call, not the outermost/entry point.
3. **Believing the call stack "pauses" during `await`** — the current async function's *remaining* work is suspended and rescheduled; the call stack frame for that function is actually removed and later re-created when execution resumes, not literally frozen in place.
4. **Long synchronous loops freezing everything** — because only one call stack exists, a long-running synchronous function (even without recursion) blocks timers, UI updates, and all other async callbacks until it finishes.

## 7. Quick Reference

```
function call    → push a frame
function returns  → pop a frame
stack too deep     → RangeError: Maximum call stack size exceeded
stack trace         → read top-to-bottom = innermost call to outermost
single stack         → single-threaded → sync code always blocks everything else
```
