The Runtime: Engine, Stack and Memory

What happens between saving a file and seeing output? The engine parses, interprets, and optimizes; calls build and unwind a stack; objects live in a heap until nothing can reach them. This machinery explains errors, leaks, and performance advice alike.

From source to running code

Parse, bytecode, JIT

JavaScript is not executed line by line. An engine — V8 in Chrome and Node, SpiderMonkey in Firefox, JavaScriptCore in Safari — processes your file in stages:

Parse: the text becomes a syntax tree. Failures here are lesson 14's SyntaxErrors — nothing has executed yet. Interpret: the tree becomes bytecode that runs immediately. Optimize: while the code runs, the engine watches which types actually arrive and compiles "hot" functions into machine code — the just-in-time (JIT) compiler at work.

// The engine cannot know at parse time what `a + b` does — JS has no static types.
// So it SPECULATES: "these were numbers last time — compile for numbers".
function add(a, b) {
  return a + b;
}

for (let i = 0; i < 1_000_000; i++) add(i, i); // hot: JIT compiles a number-fast path
console.log(add(3, 4));     // 7 — the fast path handles numbers
console.log(add("3", "4")); // "34" — a string showed up; the assumption is broken

// Expected output:
// 7
// 34

That last call deoptimizes: the engine throws away the specialized version and falls back to the general path. Nothing crashes — but the practical rule stands: keep the types a function sees stable, and it stays fast.

The call stack

Frames: one per call, last in / first out

Every call creates a frame holding that invocation's local variables and its return address. Frames stack while calls nest and pop off as functions return — which is why lesson 14's error list is called a stack trace: it is the current stack, printed.

function descend(depth) {
  if (depth === 0) return "bottom"; // base case: the stack stops growing here
  return descend(depth - 1);        // each frame stays alive while it waits
}

console.log(descend(3)); // depth 1 → bottom — after building 4 frames, unwinding them
Call stack frames next to heap objects

While descend(1) runs, three frames sit on the stack; the heap object is shared by two references.

Stack overflow — the stack has a hard limit

function runaway(n) {
  return runaway(n + 1); // no base case: every frame waits on the next
}

runaway(0);
// RangeError: Maximum call stack size exceeded

Deep recursion is the everyday cause (demo runtime/23_runtime_lab.js catches and prints this exact error). The fix is a reachable base case — or a loop, which reuses one frame instead of stacking thousands.

The heap and references

Frames are small and predictable; objects are not. Objects — arrays, functions, class instances — live in the heap, and variables hold only references to them. Lesson 04's value-versus-reference diagram is a picture of exactly this. Closures live here too: a returned function keeps its captured variables in the heap long after its own frame is gone.

Garbage collection

Reachability, not tidiness

There is no free() in JavaScript. The collector periodically frees every object that is unreachable — nothing in scope can lead to it, directly or transitively. This is why removing an element only helps if it also makes the object unreachable.

Three everyday leak patterns

// 1. Unbounded caches: every entry stays reachable, so nothing is ever collected
const cache = [];
cache.push({ key: "a", payload: new Array(1000).fill(0) });

// 2. Forgotten timers: the callback closes over big data, the timer keeps it alive
const bigData = new Array(100_000).fill("x");
setInterval(() => console.log(bigData.length), 1000); // runs forever unless cleared

// 3. Detached nodes: a removed element stays alive via a saved reference
let detached = document.querySelector("#widget"); // (browser code)
document.querySelector("#widget")?.remove();      // out of the DOM…
console.log(detached !== null);                   // …but alive in the heap,
                                                  // with every listener attached

Shape matters: hidden classes

Engines do not store property names in dictionaries — that would be slow. Objects with the same properties, created in the same order, share a hidden class and run fast, specialized code. Objects that drift (properties added one at a time, in varying orders) push the engine into slower generic paths:

// Same properties, SAME order → one shape
const point1 = { x: 1, y: 2 };
const point2 = { x: 3, y: 4 };

// Drifted shape: properties added one at a time, in a different order
const point3 = { y: 5 };
point3.z = 9;
point3.x = 7;

The practical habit: build objects with all their properties up front, in a consistent order — constructors and object literals do this naturally.

Practice: make the invisible visible

The task

Run runtime/23_runtime_lab.js with node: it builds and unwinds frames, overflows the stack on purpose, contrasts stack and heap semantics, and grows a leak you can measure. Predict each section before running. Then delete the base case in descend, predict the error, and check.

The checklist

  • You can name the pipeline stages: parse → bytecode → JIT optimize (→ deoptimize).
  • You can explain a stack trace as "the current call stack, printed".
  • You know objects live in the heap and assignment copies references, never objects.
  • You can name two leak patterns and say what keeps each one reachable.