skip to content

Execution Model

How the engine builds and stacks the environments your code runs in, and how strict mode changes the rules inside them. This is what you lean on when an interviewer asks you to trace identifier resolution instead of reciting a definition.

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

explore

questions

8

In JavaScript, what happens at runtime when code reads an identifier that no environment in the scope chain declares — for example `console.log(total)` where `total` was never declared — and how is that different from reading a variable that currently holds `undefined`?

level: juniorimportance: must knowfreq 62%

answer

  1. resolution walks outward, then stops
  2. no binding anywhere in the chain
  3. not the same as holding undefined
  4. ReferenceError, thrown on read
  5. typeof is the tolerant operator

basics

~10 s

Reading an undeclared identifier throws a ReferenceError. The engine walks the scope chain outward to the global environment, finds no binding with that name, and aborts evaluation instead of quietly returning undefined.

solid answer

~50 s

When the engine evaluates an identifier it resolves it against the scope chain: it asks the current environment record whether it has a binding of that name, and if not it follows the outer link to the enclosing environment, repeating until it reaches the global environment. If nothing has the binding, the reference is unresolvable and reading it throws a `ReferenceError: total is not defined`. That is a different failure from a variable that resolves fine but holds `undefined` — there the binding exists, so the read succeeds and you get the value `undefined`. The messages read similarly (`is not defined` vs `undefined`), which is why people confuse them, but only the first one is a scope-resolution failure. The one operator that tolerates a miss is `typeof`: `typeof total` returns the string `"undefined"` for an undeclared name instead of throwing, which is why feature-detection guards are written that way.

code

javascript · 10 lines
javascript
let declared;
console.log(declared);        // undefined — binding exists, value is undefined
console.log(typeof missing);  // "undefined" — typeof tolerates an unresolvable name

try {
  console.log(missing);       // resolution fails here
} catch (e) {
  console.log(e.constructor.name); // "ReferenceError"
  console.log(e.message);          // "missing is not defined"
}

go deeper

for a junior

Be able to say plainly that reading a name nothing declares throws a ReferenceError, while a declared variable with no value gives you undefined, and to spot which of the two a given error message describes.

for a middle

Explain the walk itself: current environment record, outer link, repeat to the global environment, first match wins. Mention that typeof is the specified exception and that assignment behaves asymmetrically from reading.

for a senior

Turn the distinction into a debugging routine — a ReferenceError points at a name (typo, missing import, load order), a TypeError on undefined points at a value — and say how you keep the loud failure rather than papering over it with defensive checks.

for a principal

Argue the design tradeoff: fail-fast name resolution buys early, precisely-located errors at the cost of runtime crashes on paths that never ran in test. Discuss where you invest to close that gap — lint rules for undefined identifiers, module boundaries, and guarding genuinely optional host globals.

## Execution contexts and environments Every time JavaScript starts evaluating code — a script, a function call, a block — it creates an *execution context*. The part of that context that matters for names is its **lexical environment**, which is two things glued together: an **environment record** (a table mapping binding names to values) and an **outer link** pointing at the environment the code was written inside. The global environment's outer link is `null`, so the links form a finite chain: this is the **scope chain**. A function's outer link is fixed when the function object is created, from the environment it was created in — not from whoever calls it later. That is what makes JavaScript scoping *lexical*. ## The resolution algorithm When the engine meets a bare identifier, it performs what the specification calls ResolveBinding. Informally: 1. Start at the running execution context's environment record. 2. Ask: does this record have a binding named `total`? 3. If yes, that is the binding — stop. 4. If no, move to the record's outer environment and repeat from step 2. 5. If the outer link is `null` and the global environment has no such binding (neither a declared global nor a property of the global object), the reference is **unresolvable**. Resolution stops at the *first* record that has the name, which is exactly why an inner declaration shadows an outer one of the same name. ## Unresolvable reference: read vs write What happens next depends on what you were doing with the reference. **Reading** an unresolvable reference throws immediately: ```js console.log(total); // ReferenceError: total is not defined ``` Evaluation of that statement aborts and the error propagates like any other throw, so the rest of the function does not run unless a `try`/`catch` catches it. **Assigning** to an unresolvable reference is the asymmetric case: in non-strict script code `total = 5` does not throw — it creates a property on the global object, an accidental global. In strict code and in modules, that assignment throws a ReferenceError too. ## "is not defined" is not "undefined" These are two distinct states that beginners collapse into one: ```js let declared; console.log(declared); // undefined — the binding exists, its value is undefined console.log(typeof missing); // "undefined" — typeof tolerates an unresolvable name console.log(missing); // ReferenceError: missing is not defined ``` In the first line, name resolution *succeeded*: the environment record has a binding called `declared`, and the value stored in it happens to be the primitive `undefined`. In the third line, name resolution *failed*: there is no such binding anywhere in the chain. The value `undefined` is a normal JavaScript value you can pass around, compare and store; an unresolvable reference is not a value at all, it is an error condition. The practical consequence is diagnostic. `TypeError: Cannot read properties of undefined` means a binding was found and its value was `undefined`; `ReferenceError: x is not defined` means the *name* itself is unknown. The second almost always means a typo, a missing import, a script that has not loaded yet, or a global your host environment does not provide. ## The typeof escape hatch `typeof` is specified to return `"undefined"` for an unresolvable reference rather than throwing. That makes it the safe probe for a name that may not exist at all: ```js if (typeof SOME_OPTIONAL_GLOBAL !== "undefined") { // safe to use it here } ``` Two caveats. First, this only covers *bare identifiers*: `typeof obj.missing` is a normal property read, and if `obj` itself is unresolvable it still throws. Second, `typeof` is not a blanket rescue for every name that looks unresolved — a `let` or `const` binding that exists in the current block but has not been initialised yet is a separate mechanism with its own rules. ## Why the loud failure is a feature A language could have made an unknown name evaluate to a null-ish value. JavaScript throws instead, and that is the behaviour you want: a typo like `usreId` fails at the exact line that contains the mistake, with the misspelled name printed in the message, rather than silently poisoning a calculation three functions later. The lesson for reading a stack trace is to trust the first frame: the identifier named in a ReferenceError is spelled exactly as it appears in the source that failed, so the fix is usually visible in that one line.

  • Why does `typeof someUndeclaredName` return a string instead of throwing, when reading the same name directly throws?
    `typeof` is specified to check for an unresolvable reference first and yield `"undefined"` rather than performing a normal value read. It exists precisely so code can probe for a name that may not exist in this environment. It only covers bare identifiers, though — `typeof missing.prop` still throws, because the property read happens after the identifier itself fails to resolve.
  • You see `TypeError: Cannot read properties of undefined (reading 'id')` instead of a ReferenceError. What does that tell you about the scope chain?
    That name resolution succeeded. Some environment in the chain does have that binding; its value is just `undefined`, and the failure came from the property access on that value. So it is not a missing declaration or a typo in the identifier — look instead at what assigned the binding, or at the function that returned nothing.
  • Does assigning to an undeclared name fail the same way as reading one?
    No — it is asymmetric. Reading an unresolvable reference always throws. Assigning to one in non-strict script code silently creates a property on the global object instead of throwing, which is how accidental globals appear. In strict code and in ES modules the assignment throws a ReferenceError, making the two operations behave alike.

saying these in an interview costs you the question

  • Says an undeclared variable evaluates to undefined
  • Confuses "x is not defined" with the value undefined
  • Thinks the engine auto-creates the binding on read
  • Claims typeof throws for unknown identifiers
  • Believes ReferenceError is a compile-time-only error

context

open as a page

What does the "use strict" directive do in JavaScript, and where exactly must it appear in a script or function body to take effect?

level: juniorimportance: must knowfreq 62%

basics

~20 s

"use strict" switches a whole script or a single function body into strict mode, a stricter dialect where silent failures throw and undeclared assignments are errors. It works only as the very first statement of that script or function body.

open as a page

In JavaScript, a function `show` is declared at the top level of a file and logs a variable `value`; a separate function `run` declares its own local `const value = 'inner'` and then calls `show()`. Which binding does `show` resolve, and what rule decides it?

level: middleimportance: must knowfreq 66%

basics

~20 s

It resolves the top-level value, not the caller's. JavaScript scoping is lexical: a function's outer environment link is fixed where the function is written in the source, so the caller's local bindings are never on its scope chain.

open as a page

In a strict-mode script, what is `this` inside a function invoked as a plain `f()`, and why does the identical code see the global object when the script is not strict?

level: middleimportance: must knowfreq 70%

basics

~20 s

In strict mode this is undefined in a plain f() call. Sloppy mode substitutes the global object whenever the incoming this value is undefined or null, and wraps a primitive this value in an object — strict mode passes it through untouched.

open as a page

In a non-strict JavaScript script, what happens when a function assigns to a name that is not declared anywhere in the scope chain — for example `function init() { cache = {}; }` — and why is it a bug worth catching?

level: middleimportance: should knowfreq 48%

basics

~20 s

The assignment walks the scope chain, finds no binding, and in non-strict script code creates a property on the global object instead of throwing. That accidental global outlives the call and is shared by every other script on the page or process.

open as a page

Why does assigning to a property of an object that was passed to Object.freeze() do nothing at all in sloppy mode, yet throw a TypeError under "use strict"?

level: middleimportance: should knowfreq 48%

basics

~10 s

The assignment fails in both dialects. Sloppy mode discards the failure and continues; strict mode reports it as a TypeError. Strict mode changes the reporting of a failed write, never whether the write succeeds.

open as a page

You add "use strict" to the top of a large legacy browser script and parts of the page stop working. What classes of failure should you expect, and how would you roll the change out safely?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Expect two classes: parse-time syntax errors that kill the entire script before a line runs, and runtime errors on code paths that previously failed quietly. Roll out per function rather than per file, and lint for undeclared globals first.

open as a page

JavaScript identifier resolution is normally static — you can tell from the source which binding a name refers to. Which two legacy constructs break that guarantee, how does each one do it, and what do engines and minifiers do in response?

level: seniorimportance: nice to knowfreq 24%

basics

~20 s

The with statement and non-strict direct eval break it. with pushes an object onto the scope chain so lookups depend on that object's properties at runtime, and direct eval can add bindings to the calling scope. Both defeat static analysis, so engines deoptimize and minifiers stop renaming.

open as a page