Appearance
3.6.1 — The JavaScript Execution Model
JavaScript deserves a deep dive more than any other language, for a simple reason: it is the only language that runs natively in every browser on Earth, and — via Node.js — it also runs a large share of the world's servers. It was famously designed in ten days in 1995 by Brendan Eich at Netscape, under orders to "make it look like Java," and it carries the scars of that haste in its quirks. Yet it also carries genuinely excellent ideas — first-class functions, closures, prototypes — and thirty years of engineering have made it astonishingly fast.
This first sub-chapter builds the execution model: how JavaScript actually runs your code. What happens when a function is called, what a scope really is, why variables mysteriously exist before you declare them (hoisting), and what strict mode changes. Everything here is the foundation the rest of the folder stands on: closures (3.6.2), this (3.6.3), prototypes, modules, the event loop, and V8 internals all resolve back to the machinery built here.
1. Execution contexts and the call stack
When the engine runs your code, it doesn't just execute lines — it creates an execution context for each unit of running code: a bookkeeping structure holding everything that code needs. There are two kinds: one global execution context created when your program starts, and a new function execution context created every time a function is called.
Each context holds three things: (1) the variable environment — the variables and function declarations belonging to this code; (2) a reference to its outer environment — the scope it was defined inside, which is the key to closures below; and (3) the value of this.
These contexts are managed on the call stack — the very stack from 2.2, now visible at the language level. Calling a function pushes its context; returning pops it:
This makes two everyday phenomena obvious. A stack trace in an error message is literally a printout of this stack — the chain of calls that led to the error, innermost first. And RangeError: Maximum call stack size exceeded is the stack overflow of 2.2: infinite recursion pushed contexts until the stack ran out of room.
Crucially, JavaScript has one call stack — it is single-threaded. Only one thing executes at a time, and if a function takes ten seconds, nothing else runs for ten seconds (in a browser: the page freezes). That constraint is the entire reason the event loop exists, and we'll take it up in Chapter 3.6.3.
2. Scope: where a name is visible
Scope is the region of a program where a name is accessible. JavaScript has three levels, and the modern rules are simple once stated.
- Global scope — declared outside any function; visible everywhere.
- Function scope —
vardeclarations and function parameters are visible throughout the entire function they're declared in (regardless of blocks). - Block scope —
letandconstare visible only within the nearest{ }block (anif, a loop body, or any braces).
That difference between var and let is a genuine historical wart worth seeing, because it explains a classic bug:
javascript
for (var i = 0; i < 3; i++) { /* … */ }
console.log(i); // 3 — var leaked out of the loop!
for (let j = 0; j < 3; j++) { /* … */ }
console.log(j); // ReferenceError — let stayed inside the blockvar is function-scoped, so the loop variable escapes the loop entirely; let (added in ES6/2015) is block-scoped and behaves as every other modern language does. The practical rule the community settled on: use const by default, let when you must reassign, and var never — const communicates that a binding won't be reassigned, which makes code easier to reason about (note it prevents reassignment, not mutation: a const object's properties can still change).
When code references a name, the engine looks in the current scope; if not found, it follows the outer environment reference to the enclosing scope, and onward outward — the scope chain — until it finds the name or reaches global and throws a ReferenceError. Critically, this chain is determined by where code is written, not where it is called from — a property called lexical scoping (from "lexical" = relating to the text). Fix that idea firmly; the next section depends entirely on it.
3. Hoisting and the temporal dead zone
Before executing a scope, the engine performs a creation phase in which it scans for declarations and sets them up. This produces the behaviour called hoisting — declarations act as though "lifted" to the top of their scope. But var, let/const, and functions hoist differently, and knowing the distinction resolves several confusing errors:
javascript
console.log(a); // undefined — var is hoisted and initialised to undefined
var a = 1;
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 2;
greet(); // works! — function declarations are fully hoisted
function greet() { console.log("hi"); }var declarations are hoisted and pre-set to undefined — so reading one early gives undefined rather than an error (a silent source of bugs). Function declarations are hoisted completely, body and all, which is why you may call a function defined further down the file. let and const are also hoisted, but deliberately left uninitialised: from the top of the block until the declaration line, they exist but cannot be touched — a region called the temporal dead zone (TDZ). The TDZ is a feature: it converts a silent undefined into a loud error, catching the "used before defined" mistake at the point it happens.
4. Strict mode: the saner dialect
One more execution-model switch matters before we go further. In 2009, ES5 introduced strict mode — an opt-in dialect that removes the most dangerous legacy behaviors. A file or function beginning with the string literal "use strict" runs strict; ES modules and class bodies are strict automatically, so modern code is effectively always strict. What it changes, concretely:
javascript
"use strict";
undeclaredVar = 5; // ReferenceError — sloppy mode would CREATE a global
delete Object.prototype; // TypeError — sloppy mode would fail silently
function f(a, a) {} // SyntaxError — duplicate parameters bannedAnd the one you'll meet constantly: in a plain function call, this is undefined under strict mode, instead of silently becoming the global object — turning a whole class of "accidentally wrote to a global" bugs into immediate errors (3.6.3 builds the full this story). The theme is the same as the TDZ: convert silent wrongness into loud failure at the point of the mistake. When this book says "in strict mode," it is describing the behavior of all modern JavaScript you will write.
5. Where the model leads
Everything else famous about JavaScript is a consequence of the machinery on this page, and each consequence gets its own full treatment next:
- Lexical scoping + returned functions ⇒ closures — functions that remember their birth scope. 3.6.2 develops the mechanism, the whole pattern catalog (private state, factories, memoization, debounce), the loop trap, stale closures, and the memory model.
- The one non-lexical binding ⇒
this— resolved by the call site, with five rules and a precedence order. 3.6.3 makes every case derivable. - Weak typing ⇒ coercion —
!!,==vs===, truthiness, and the algorithms that make[] == falsederivable rather than memorizable: 3.6.7.
6. The expert lens
Lexical scoping is the quiet foundation of everything. Because scope is determined by where code is written, the engine can resolve names statically, closures (3.6.2) can capture reliably, and you can reason about a function by reading outward from it. this is the one construct that breaks this rule — it's dynamically bound (3.6.3) — and that single inconsistency is the source of a disproportionate share of JavaScript's confusion. When you notice a language feature causing chronic confusion, it's often because it violates a rule the rest of the language follows.
The creation/execution split is the engine's own two-pass compiler. Hoisting isn't a quirk bolted on — it falls out of the engine scanning a scope for declarations before running it (3.1's parse-then-execute, in miniature). Understanding it that way converts three memorized behaviors (var → undefined, functions → callable early, let → TDZ) into one mechanism with three policies. Most "JavaScript is weird" items dissolve the same way: find the single mechanism, and the cases become derivable.
JavaScript's quirks are almost all backward-compatibility debt — and that constraint is instructive. var's function scoping, sloppy mode's global-this, typeof null === "object" (a bug from 1995) — none survive because anyone defends them; they survive because the web cannot break. Millions of pages depend on existing behaviour, so the language may only add, never fix — hence let/const alongside var, strict mode alongside sloppy, arrow functions alongside regular ones. This is the strictest real-world example of backward compatibility as a design constraint (Part 10 revisits it for APIs), and the lesson generalises: once an interface has users you don't control, its mistakes become permanent — so the cost of a bad early design decision is far higher than it appears.
Next: the payoff of lexical scoping — 3.6.2 builds closures completely: mechanism, pattern catalog, traps, and memory model.
Recall
- The engine creates an execution context per running unit (global + one per call), each holding its variables, a reference to its outer environment, and
this. Contexts live on the single call stack — hence stack traces, stack-overflow errors, and JavaScript being single-threaded. - Scope:
varis function-scoped (leaks out of blocks),let/constare block-scoped. Name lookup walks the scope chain outward. Scope is lexical — fixed by where code is written. - Hoisting falls out of the engine's creation phase (scan declarations, then execute):
varis hoisted and pre-set toundefined; function declarations hoist entirely;let/consthoist but stay uninitialised in the temporal dead zone, turning a silentundefinedinto a loud error. - Strict mode (
"use strict"; automatic in modules and classes) removes legacy hazards: assigning to undeclared names throws instead of creating globals, plain-callthisisundefinedinstead of the global object, silent failures become errors. - Consequences with their own pages: lexical scoping ⇒ closures (3.6.2); the one dynamic binding ⇒
this(3.6.3); weak typing ⇒ coercion (3.6.7).
Self-test: What three things does an execution context hold? Why does let in a loop behave differently from var? What is the temporal dead zone for? Name three things strict mode changes and the design theme they share. What single engine behavior explains all three hoisting policies?
Quiz Bank
FoundationalWhat is an execution context and what is the call stack?
An execution context is the bookkeeping structure the engine creates for each unit of running code — one global context at program start, plus a new function context for every call. Each holds (1) its variable environment (its variables and function declarations), (2) a reference to its outer environment (the scope it was defined in — the basis of closures), and (3) the value of this. These contexts are managed on the call stack: a call pushes a context, a return pops it, and the top is what's executing. A stack trace is a printout of this stack; "Maximum call stack size exceeded" is infinite recursion never popping. JavaScript has exactly one call stack — it is single-threaded.
FoundationalWhat is the difference between var, let, and const?
var is function-scoped — visible throughout the entire function regardless of blocks, so a loop variable leaks out of its loop — and is hoisted and initialised to undefined, so reading it before its declaration silently yields undefined. let and const are block-scoped (visible only inside the nearest { }), and though hoisted, they remain uninitialised in the temporal dead zone until their declaration, so early access throws a clear ReferenceError. const additionally forbids reassignment of the binding (though a const object's properties may still be mutated). Modern practice: const by default, let when reassignment is needed, var never.
AppliedWhat does strict mode change, and why is modern code effectively always strict?
Strict mode (ES5, 2009) is the opt-in dialect that converts silent legacy hazards into loud errors: assigning to an undeclared name throws a ReferenceError instead of creating a global variable; this in a plain function call is undefined instead of the global object (killing the accidental-global-write class of bugs — 3.6.3); failed assignments (to read-only or non-extensible properties) and failed deletes throw instead of silently doing nothing; duplicate parameter names are a SyntaxError; with is banned. Opt in with the "use strict" directive per file or function — but ES modules and class bodies are strict automatically, which is why all modern JavaScript (anything written as modules) runs strict without anyone typing the directive. Design theme, same as the TDZ: fail loudly at the point of the mistake rather than corrupting state silently.
InterviewWhat is the temporal dead zone and why is it considered a feature?
The temporal dead zone (TDZ) is the region from the start of a block until a let/const declaration is executed, during which the variable exists (it was hoisted) but cannot be accessed — any read throws ReferenceError: Cannot access 'x' before initialization. It's a deliberate improvement over var, which hoists and initialises to undefined, so using a var too early silently yields undefined and the bug surfaces later, far from its cause. The TDZ converts that silent wrong value into an immediate, precise error at the exact point of misuse — the same philosophy as static typing (3.3): fail loudly and early rather than quietly and late.
StaffWhy does JavaScript retain so many confusing behaviours (var, ==, typeof null), and what general engineering lesson does that carry?
Because the web cannot break backward compatibility. JavaScript runs on billions of pages the language's maintainers do not control and cannot update; if a new engine version changed =='s coercion rules or made var block-scoped, an unknowable number of existing sites would silently break. So the language may only add, never fix: let/const were added beside var, === beside ==, arrow functions beside regular ones, and outright bugs like typeof null === "object" (a 1995 implementation artifact) are frozen permanently. The general lesson — and it applies far beyond JavaScript — is that once an interface has users you don't control, its mistakes become effectively permanent, so early design decisions carry a cost vastly out of proportion to the effort of making them.
Practically this argues for: shipping the smallest public surface you can, versioning explicitly (Part 9's API versioning, Part 10's backward-compatible evolution), preferring additive change, and treating "we'll fix it later" as false for anything published. It also explains why languages accumulate parallel mechanisms over time and why style guides ("always ===", "never var") exist — the community enforces by convention what the language cannot enforce by removal.
Flashcards
FlashWhat an execution context holds
Its variable environment, a reference to its outer environment (for scope/closures), and the value of this. Managed on the call stack.
Flashvar vs let/const scoping
var: function-scoped, hoisted as undefined. let/const: block-scoped, hoisted but in the temporal dead zone until declared.
FlashLexical scoping
Scope is determined by where code is written, not where it's called — which is what makes closures possible.
FlashHoisting in one mechanism
Creation phase scans declarations before execution. Policies: var → undefined; function declarations → fully callable; let/const → TDZ until their line.
FlashStrict mode essentials
Modules/classes are strict automatically. Undeclared assignment throws; plain-call this is undefined; silent failures become errors; duplicate params banned.
Scenario Drill
DrillA teammate reports two mysteries in one file: calling a helper defined at the bottom works in one place but throws TypeError: helper2 is not a function in another; and a config variable logs undefined at the top of the file even though it is assigned on line 40. Explain both with the creation/execution model and prescribe the cleanup.
Both mysteries are the creation-phase policies in action. Mystery 1: the working helper is a function declaration (function helper() {…}) — hoisted completely, body and all, so calls anywhere in the file succeed. The failing one is a function expression assigned to a variable (var helper2 = function () {…} or const helper2 = () => {…}). With var, the name hoists initialized to undefined, so calling it before the assignment line is undefined() — exactly the reported TypeError: helper2 is not a function; with const, the call would instead throw a TDZ ReferenceError. Same file, two declaration forms, two different hoisting policies.
Mystery 2: config is declared with var somewhere below, so during the creation phase the name exists file-wide pre-set to undefined; the early console.log(config) reads that placeholder — a silent wrong value rather than an error, which is precisely the failure mode let/const's temporal dead zone was designed to eliminate.
Cleanup prescription: (1) convert var to const/let throughout — early reads then fail loudly at the mistake instead of propagating undefined; (2) adopt a declare-before-use ordering so hoisting never carries meaning (lint: no-use-before-define, no-var, vars-on-top equivalents); (3) if the file relies on calling helpers "above" their definitions, keep them as function declarations deliberately — that's the one hoisting behavior that is genuinely safe and idiomatic — but make the choice explicit in the team's style guide rather than accidental. The teaching point for the team: hoisting is one mechanism (scan, then run) with three policies; once everyone can name the policy per declaration form, both "mysteries" become predictions.