Chapter 16

Execution Context Stack and Realm: Call Stack Structure and Cross-Realm Traps

Every function call creates an independent execution contextโ€”this is the foundation of JavaScript's runtime model

JavaScript engines do not execute code by "scanning line by line." Instead, they maintain an Execution Context Stack (EC Stack, also called the Call Stack). Each time a function is called, the engine pushes a new Execution Context (EC) onto the top of the stack. When the function finishes, that EC is popped off, and control returns to the EC below it. This push/pop cycle governs variable visibility, this values, and execution order.

Realm is a separate concept: it is a complete JavaScript runtime environment containing its own global object and a full set of built-in constructors. The main document, an <iframe>, and a Web Worker each hold an independent Realm. When objects cross Realm boundaries, instanceof silently failsโ€”this is not a bug but an intentional consequence of the specification design.


๐Ÿ”น Level 1 ยท What You Need to Know

The Call Stack: Order of Function Entry and Exit

The call stack is a Last-In-First-Out (LIFO) data structure. When a program starts, the engine creates the Global Execution Context (Global EC) and pushes it to the bottom of the stack, where it stays for the entire program lifetime until the page closes or the process exits.

function bar() {
  console.log('bar executing');
}

function foo() {
  bar();
  console.log('foo executing');
}

foo();

The call stack changes for this code:

  1. Program starts โ†’ push Global EC
  2. foo() is called โ†’ push foo EC
  3. bar() is called โ†’ push bar EC
  4. bar finishes โ†’ pop bar EC
  5. foo finishes โ†’ pop foo EC
  6. Program ends โ†’ pop Global EC

Each EC is independent: local variables in foo are not visible to bar (unless through a closure, covered in Chapter 19).

Stack Overflow: The Consequence of Unbounded Recursion

The call stack has a depth limit. Chrome (V8) allows roughly 10,000โ€“15,000 frames, Firefox (SpiderMonkey) roughly 50,000, and Node.js roughly 10,000โ€“12,000 (varies with the number of parameters in the recursive function). Exceeding the limit throws:

RangeError: Maximum call stack size exceeded

The most common trigger is recursion without a termination condition:

// Wrong: no termination condition
function countdown(n) {
  console.log(n);
  countdown(n - 1); // never stops
}
countdown(5); // RangeError

// Correct: termination condition present
function countdown(n) {
  if (n < 0) return; // base case
  console.log(n);
  countdown(n - 1);
}
countdown(5); // 5, 4, 3, 2, 1, 0

The most direct way to debug the call stack is to set a breakpoint in the browser DevTools "Sources" panel; the "Call Stack" panel on the left shows the complete EC stack at that moment.

Realm: An Independent JavaScript Runtime Environment

A Realm is a container that holds:

Key conclusion: The Array in different Realms is a different constructor function. An array created inside an iframe will fail an instanceof Array check in the main document:

// Main document
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const iframeArray = new iframe.contentWindow.Array(1, 2, 3);

console.log(iframeArray instanceof Array);                // false โ† cross-Realm failure
console.log(Array.isArray(iframeArray));                  // true  โ† safe cross-Realm check
console.log(Object.prototype.toString.call(iframeArray)); // "[object Array]" โ† also safe

Array.isArray uses the [[IsArray]] internal operation. It recursively unwraps Proxy wrappers and checks the internal slot directly, without traversing any prototype chain, so it is immune to Realm isolation. This is the specification's intentional fix for cross-Realm scenarios.


๐Ÿ”ธ Level 2 ยท How It Actually Works

The Three Core Components of an Execution Context

The ECMAScript specification defines that every execution context contains the following core state components (specific EC types may add more):

Component Meaning Primary Use Case
LexicalEnvironment Current lexical environment; lookup starting point for let/const/function/class Variable lookup, TDZ
VariableEnvironment Environment for var declarations; usually the same as LexicalEnvironment var hoisting
ThisBinding The this value bound to this EC Method calls, arrow functions
Realm Record The Realm this EC belongs to Accessing built-in objects
ScriptOrModule The Script or Module Record currently running ESM import.meta

LexicalEnvironment and VariableEnvironment only diverge inside try...catch blocks and with statementsโ€”a catch block creates a new LexicalEnvironment (to bind the error variable) while VariableEnvironment stays unchanged. In 99% of cases they point to the same Environment Record.

Full Structural Diagram of the Call Stack

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                     EC Stack (Call Stack)                    โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚  โ”‚  bar EC  (top - currently executing)                 โ”‚   โ”‚
โ”‚  โ”‚  โ”œโ”€ LexicalEnvironment  โ†’ bar's ER                   โ”‚   โ”‚
โ”‚  โ”‚  โ”œโ”€ VariableEnvironment โ†’ bar's ER                   โ”‚   โ”‚
โ”‚  โ”‚  โ””โ”€ ThisBinding โ†’ undefined (strict) / global (non)  โ”‚   โ”‚
โ”‚  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค   โ”‚
โ”‚  โ”‚  foo EC                                              โ”‚   โ”‚
โ”‚  โ”‚  โ”œโ”€ LexicalEnvironment  โ†’ foo's ER                   โ”‚   โ”‚
โ”‚  โ”‚  โ”œโ”€ VariableEnvironment โ†’ foo's ER                   โ”‚   โ”‚
โ”‚  โ”‚  โ””โ”€ ThisBinding โ†’ undefined / global                 โ”‚   โ”‚
โ”‚  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค   โ”‚
โ”‚  โ”‚  Global EC  (bottom - always present)                โ”‚   โ”‚
โ”‚  โ”‚  โ”œโ”€ LexicalEnvironment  โ†’ GlobalEnvironmentRecord    โ”‚   โ”‚
โ”‚  โ”‚  โ”œโ”€ VariableEnvironment โ†’ GlobalEnvironmentRecord    โ”‚   โ”‚
โ”‚  โ”‚  โ””โ”€ ThisBinding โ†’ globalThis                         โ”‚   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Full Contents of a Realm

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                        Realm Record                          โ”‚
โ”‚                                                             โ”‚
โ”‚  [[Intrinsics]]   โ†’ a map storing all built-in objects      โ”‚
โ”‚  โ”œโ”€ %Array%       โ†’ Array constructor                       โ”‚
โ”‚  โ”œโ”€ %Object%      โ†’ Object constructor                      โ”‚
โ”‚  โ”œโ”€ %Function%    โ†’ Function constructor                    โ”‚
โ”‚  โ”œโ”€ %Promise%     โ†’ Promise constructor                     โ”‚
โ”‚  โ””โ”€ ... (~200 built-in objects)                             โ”‚
โ”‚                                                             โ”‚
โ”‚  [[GlobalObject]]  โ†’ global object (window / global / self) โ”‚
โ”‚  [[GlobalEnv]]     โ†’ GlobalEnvironmentRecord                โ”‚
โ”‚  [[TemplateMap]]   โ†’ template literal cache                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Each Realm's [[Intrinsics]] table is independent. This is why iframe.contentWindow.Array !== window.Array, even though they behave identically.

How Generator Function ECs Suspend

Generators are a special case in execution context management. When gen.next() is called, the Generator's EC is pushed onto the stack. When execution reaches yield, the EC is not popped and destroyedโ€”it is saved into the GeneratorObject's internal slot [[GeneratorContext]] and then removed from the stack. The next call to next() restores this EC to the top of the stack to continue execution.

function* counter() {
  console.log('segment 1');
  yield 1;           // EC suspends here, saved to GeneratorObject
  console.log('segment 2');
  yield 2;           // EC suspends again
  console.log('segment 3');
  return 3;          // EC completes, GeneratorObject state โ†’ 'completed'
}

const gen = counter();  // creates GeneratorObject; EC has not run yet
gen.next();  // push EC, run to yield 1, EC suspends โ†’ { value: 1, done: false }
gen.next();  // restore EC, run to yield 2, EC suspends โ†’ { value: 2, done: false }
gen.next();  // restore EC, run to return, EC destroyed โ†’ { value: 3, done: true }

A Generator EC's lifecycle is not tied to a single call; it survives across multiple next() calls and is only truly destroyed when done: true.

Tail Call Optimization (TCO) and EC Reuse

A tail call is one where the last operation of a function is a call to another function, and the result is returned directly without any further computation. ECMAScript 2015 mandates in strict mode that engines must perform tail call optimization: reuse the current EC rather than create a new one, preventing stack growth.

'use strict';

// True tail call: the last step is return factorial(n-1, acc*n), nothing after
function factorial(n, acc = 1) {
  if (n <= 1) return acc;
  return factorial(n - 1, acc * n); // tail call: engine can reuse current EC
}

// Not a tail call: return n * factorial(n-1) requires multiplication after the call
function badFactorial(n) {
  if (n <= 1) return 1;
  return n * badFactorial(n - 1); // non-tail-call: current EC must be preserved
}

Implementation status (2025): Safari (JavaScriptCore) fully implements TCO; V8 (Chrome/Node.js) explicitly does not implement TCO, citing high maintenance cost and debugging difficulty, even in strict mode. Therefore, you cannot rely on TCO to avoid stack overflow. The correct approach is to rewrite recursion as iteration, or use the Trampoline technique.


๐Ÿ”บ Level 3 ยท What the Specification Says

Specification Section 9.4: Execution Contexts

ECMAScript specification (ECMA-262, Section 9.4) defines an execution context as:

An execution context is a specification device that is used to track the runtime evaluation of code by an ECMAScript implementation.

State components of every execution context:

Component Type Description
code evaluation state โ€” Current state (executing/suspended/completed); used by Generators and Async functions
Function Object / null The function object currently executing (null if at script/module level)
Realm Realm Record The associated Realm Record
ScriptOrModule Script / Module Record The associated Script or Module Record
LexicalEnvironment Environment Record โ€”
VariableEnvironment Environment Record โ€”
PrivateEnvironment PrivateEnvironment Record Used for class private fields

The specification describes the call stack as: maintaining an execution context stack, where the top entry is the running execution context. All spec operations that refer to "the current lexical environment" mean "the LexicalEnvironment of the running execution context."

Specification Section 9.3: Realms

The specification defines these core operations:

CreateRealm() algorithm (simplified):

  1. Create a new Realm Record realmRec
  2. Call CreateIntrinsics(realmRec) โ€” populate the [[Intrinsics]] map (creates all built-in object prototypes and constructors, ~200 entries)
  3. Set realmRec.[[GlobalObject]] to undefined (set in a later step)
  4. Set realmRec.[[GlobalEnv]] to undefined (set in a later step)
  5. Set realmRec.[[TemplateMap]] to an empty list
  6. Return realmRec

SetRealmGlobalObject(realmRec, globalObj, thisValue) algorithm:

  1. If globalObj is undefined, create a new ordinary object as the global object
  2. Set realmRec.[[GlobalObject]] to globalObj
  3. Create a new GlobalEnvironmentRecord based on globalObj
  4. Set realmRec.[[GlobalEnv]] to that GlobalEnvironmentRecord

Internal Structure of GlobalEnvironmentRecord

GlobalEnvironmentRecord is a composite of two sub-records:

GlobalEnvironmentRecord
โ”œโ”€ [[ObjectRecord]]      โ†’ ObjectEnvironmentRecord (bound to globalObj)
โ”‚   โ””โ”€ var declarations โ†’ become properties on globalObj
โ””โ”€ [[DeclarativeRecord]] โ†’ DeclarativeEnvironmentRecord
    โ””โ”€ let/const/class  โ†’ stored in DeclarativeRecord

This is why var x = 1 at global scope makes window.x === 1 true, but let x = 1 at global scope leaves window.x === undefined. The two declaration forms land in different sub-records within GlobalEnvironmentRecord.

Specification text (9.1.1.4 GetBindingValue, simplified): GlobalEnvironmentRecord looks up a variable by first checking [[DeclarativeRecord]] (for let/const), and if not found, checking [[ObjectRecord]] (for var and global object properties).

The [[IsArray]] Internal Operation and Cross-Realm Detection

Specification Section 7.2.2 defines the IsArray(argument) abstract operation:

  1. If argument is an Array exotic object โ†’ return true
  2. If argument is a Proxy exotic object โ†’ call IsArray(argument.[[ProxyTarget]]) (recursively unwrap)
  3. Otherwise โ†’ return false

The key point: IsArray checks the internal [[Class]] slot of the object (an Array exotic object is characterized by its special [[DefineOwnProperty]]), not the prototype chain. By contrast, instanceof walks OrdinaryHasInstance โ†’ the prototype chain, which is necessarily Realm-specific.

The specification text of Array.isArray:

  1. Return ? IsArray(arg).

Just one lineโ€”all the magic lives in the IsArray abstract operation.


๐Ÿ’Ž Level 4 ยท Edge Cases and Traps

Trap 1: Cross-iframe instanceof Silently Fails

Symptom: The parent page passes an array to an iframe; code inside the iframe uses instanceof Array to detect it, gets false, and takes the wrong code path.

// Parent page (parent.html)
const iframe = document.createElement('iframe');
iframe.src = 'child.html';
document.body.appendChild(iframe);

iframe.onload = function() {
  const arr = [1, 2, 3];
  iframe.contentWindow.processData(arr);
};

// Code inside child.html
function processData(data) {
  if (data instanceof Array) {
    // Array created in parent โ€” instanceof fails here!
    console.log('is array:', data.length);
  } else {
    console.log('not an array'); // โ† this branch executes
  }
}

Root cause: The [1,2,3] from the parent was created using the parent Realm's %Array%; the Array inside the iframe is from a different Realm's %Array%. instanceof checks: arr.__proto__ === Array.prototype โ€” here Array.prototype is the iframe's version, while arr.__proto__ is the parent's version, and they are different objects.

Correct approach:

function processData(data) {
  if (Array.isArray(data)) {        // safe: uses [[IsArray]] internal operation
    console.log('is array:', data.length);
  }

  // Or use Object.prototype.toString (also Realm-agnostic)
  if (Object.prototype.toString.call(data) === '[object Array]') {
    console.log('is array');
  }
}

The same issue applies to instanceof Error, instanceof Map, and every other constructor across Realm boundaries. Always avoid instanceof for cross-Realm checks; use toString or dedicated static methods like Array.isArray.

Trap 2: New Realms Created by the Node.js vm Module

Node.js's vm module runs code inside an isolated sandbox; each vm.createContext() creates a new Realm.

const vm = require('vm');

const sandbox = { x: 10 };
const context = vm.createContext(sandbox);

// Create an array inside the sandbox
const result = vm.runInContext('[1, 2, 3]', context);

console.log(result instanceof Array);        // false โ† cross-Realm
console.log(Array.isArray(result));          // true  โ† safe
console.log(result.constructor === Array);   // false โ† cross-Realm
console.log(result.constructor.name);        // 'Array' โ† same name, different object

// Prototype of the object created inside the sandbox
const resultProto = vm.runInContext(
  'Object.getPrototypeOf([1,2,3])',
  context
);
console.log(resultProto === Array.prototype); // false โ† different Realm prototypes

Practical engineering impact: In plugin systems or sandbox execution of user code in Node.js, any object returned from the sandbox may trigger this problem. The solution is to serialize (JSON.stringify/JSON.parse) data across Realm boundaries, or use structuredClone.

Trap 3: TCO Engine Differences Lead to Portability Problems

ECMAScript 2015 requires TCO in strict mode, but V8 explicitly refuses to implement it, causing the same code to behave differently in Safari and Chrome:

'use strict';

// In Safari: handles any depth of n (TCO active, no stack growth)
// In Chrome/Node.js: RangeError when n > ~12000
function sum(n, acc = 0) {
  if (n === 0) return acc;
  return sum(n - 1, acc + n); // tail call
}

console.log(sum(100000)); // Safari: 5000050000; Chrome: RangeError

Portable rewriteโ€”use a loop instead of tail recursion:

function sum(n) {
  let acc = 0;
  for (let i = n; i > 0; i--) {
    acc += i;
  }
  return acc;
}

// Or the Trampoline technique (useful for more complex mutual recursion)
function trampoline(fn) {
  return function(...args) {
    let result = fn(...args);
    while (typeof result === 'function') {
      result = result();
    }
    return result;
  };
}

const sumTramp = trampoline(function _sum(n, acc = 0) {
  if (n === 0) return acc;
  return () => _sum(n - 1, acc + n); // return a function instead of calling directly
});

console.log(sumTramp(100000)); // 5000050000, safe on any engine

Trap 4: Call Stack Depth Varies Across Engines

Call stack depth is not a fixed value mandated by the ECMAScript specificationโ€”each engine decides it independently:

Engine Version Typical Stack Depth Factors
V8 (Chrome/Node.js) Chrome 120 / Node 20 10,000โ€“15,000 Number of function parameters (more params = larger frame = lower depth)
SpiderMonkey (Firefox) Firefox 121 ~50,000 Optimization level
JavaScriptCore (Safari) Safari 17 ~65,000 (unlimited with TCO) TCO implementation
Hermes (React Native) 0.73 ~5,000 Mobile memory constraints
// Measure the current engine's call stack depth
function measureStackDepth(depth = 0) {
  try {
    return measureStackDepth(depth + 1);
  } catch (e) {
    return depth;
  }
}

console.log(`Current engine call stack depth: ${measureStackDepth()}`);
// V8: ~13000; SpiderMonkey: ~50000; JSC: ~65000

This variance means that deeply recursive code working fine in Firefox can crash immediately in Hermes (React Native). Cross-platform applications should target the most conservative engine; a safe ceiling is around 3,000 frames.

Trap 5: Suspended Generator ECs and Memory Impact

An incomplete Generator object holds a suspended EC; the EC holds its LexicalEnvironment; the LexicalEnvironment may hold large variables. If the Generator object is forgotten (neither next() nor return() is called again), the suspended EC cannot be garbage-collected.

function* heavyGenerator() {
  const bigBuffer = new Uint8Array(10 * 1024 * 1024); // 10 MB
  yield 'midpoint';
  // bigBuffer remains held by the EC after yield, until the Generator finishes
  yield bigBuffer.length;
}

// Danger: only one next() call; Generator is stuck in the middle; bigBuffer cannot be GC'd
const gen = heavyGenerator();
gen.next(); // { value: 'midpoint', done: false }
// If gen is then forgotten, 10 MB is leaked

// Correct: either iterate to completion or explicitly close
const gen2 = heavyGenerator();
gen2.next();
gen2.return(); // force-close the Generator; EC destroyed; memory released

For async iterators (for await...of), the specification requires that engines call the iterator's return() method on a break, ensuring Generator resources are released. However, manually created Generators that are abandoned mid-way do not trigger return() automatically.


Summary

  1. Every function call creates an Execution Context (EC) pushed onto the call stack; when the function returns, the EC is popped. The call stack has an engine-imposed depth limit: V8 allows roughly 10,000โ€“15,000 frames, SpiderMonkey roughly 50,000.

  2. An Execution Context contains three core components: LexicalEnvironment (for let/const scope), VariableEnvironment (for var scope), and ThisBinding (the this value). In the vast majority of cases the first two point to the same Environment Record.

  3. A Realm is an independent JavaScript runtime environment containing its own global object and a complete set of built-in constructors; iframes, Workers, and Node.js vm contexts each hold an independent Realm.

  4. Cross-Realm instanceof checks silently fail; the correct approach is Array.isArray() (which uses the [[IsArray]] internal operation) or Object.prototype.toString.call().

  5. When a Generator function reaches yield, its EC is saved to the GeneratorObject's internal slot and removed from the call stack; an incomplete Generator holds a live EC that prevents its LexicalEnvironment from being garbage-collectedโ€”abandoning a Generator mid-way requires calling generator.return() to explicitly release resources.

Rate this chapter
4.5  / 5  (16 ratings)

๐Ÿ’ฌ Comments