Case 12/13

Prefer reading? The full explanation

How the TDZ Works

hoisting: var vs let · Browser

After this you can explain

What hoisting actually hoists, and why reading a let before its declaration throws while var quietly reads undefined.

console.log('1:', typeof v);
var v = 'set';

try {
  console.log(l);
} catch (e) {
  console.log('2:', e.constructor.name);
}
let l = 'set';

console.log('3: done');

Output: 1: undefined → 2: ReferenceError → 3: done

What runs before your first line

The output is 1: undefined → 2: ReferenceError → 3: done. To predict it you need one model: before executing a scope, the engine scans it and registers every declaration. "Hoisting" is that registration — no code moves anywhere.

The registration differs by keyword. var v is created AND initialized to undefined. let l is created but left uninitialized. function declarations are created fully initialized, body and all — the experiment shows one being called above its declaration.

The dead zone, precisely

The temporal dead zone is the stretch between entering the scope and executing the declaration line, during which the binding exists in an uninitialized state. Reading or writing it throws ReferenceError: Cannot access 'l' before initialization.

It is temporal, not spatial: what matters is whether the declaration has executed, not where the code sits in the file. A function defined above the declaration but called after it reads the binding fine.

Why typeof behaves differently on line 1 and line 5

typeof v on line 1 is safe because v is an initialized binding holding undefined. But typeof l before line 9 would throw just like the plain read did — the TDZ blocks every access, typeof included.

That makes TDZ variables stricter than variables that do not exist at all: typeof completelyUndeclared returns "undefined" without complaint. An odd corner, but a useful one to know when a typeof check suddenly starts throwing after a refactor from var to let.

Why the language does this

The TDZ exists to turn a silent bug into a loud one. Under var semantics, reading before assignment gives you undefined that flows onward and fails somewhere far from the cause. Under let/const semantics the same mistake fails at the exact line that made it.

Practical consequence: declare close to first use, and treat "cannot access before initialization" as a precise pointer — some code above that line runs earlier than you thought it did.

Reading about it is one thing. Watching it run is another.

↑ Run it step by step

In the wild

Where this rule bites real code

The refactor that made typeof throw

Symptom
A legacy typeof settings !== "undefined" guard worked for years. Someone modernises var settings to let settings — and the guard itself starts throwing.
Why
typeof is only safe on undeclared names and initialized bindings. A let binding in its TDZ throws on every access, typeof included.
Fix
Kill order-dependence instead of guarding it: declare before use, or restructure so the check does not rely on whether a later declaration has run.

Environment

Browser

CI verified

Node.js v22.22.3

Last reviewed

29 July 2026

The animation on this page is a teaching model, not a diagram of browser internals. Real engines use several task queues, optimise aggressively, and do plenty of work these panels do not show. What is guaranteed is the observable ordering: the stated output is produced by actually executing the code in CI, on the runtime named above. Browser-specific behaviour follows the HTML Standard and is not executed in a real browser by CI. How this is verified →