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