The Temporal Dead Zone
Introduction
The Temporal Dead Zone (TDZ) is the period between entering a scope and initializing a let, const, or class binding. The name exists, but JavaScript refuses to let you read or write it yet.
This is why let and const feel stricter than var: early access fails loudly instead of returning undefined.
Why This Matters
TDZ questions separate memorization from real understanding. Senior interviewers often use shadowing, typeof, class declarations, and default parameters to see whether you know that lexical bindings are created early but initialized later.
Theory
The precise rule
For let, const, and class, the binding is created when the scope is entered. It is initialized only when execution reaches the declaration. Any access before initialization throws ReferenceError.
TDZ is about time, not just position
The word temporal matters. A line of code may appear after a declaration but still run before it due to a function call, or it may appear before a declaration but never run. TDZ errors happen at runtime when an access occurs before initialization.
Shadowing can create surprising TDZ errors
An inner let shadows an outer variable for the entire block, including lines before the inner declaration. So a read at the top of the block does not fall back to the outer variable; it hits the inner uninitialized binding and throws.
typeof exception has an exception
typeof notDeclared returns 'undefined' for a truly undeclared name. But typeof tdzName throws ReferenceError if tdzName is a lexical binding in the current scope that has not initialized yet.
Default parameter TDZ
Parameters are initialized left to right. A default value can read earlier parameters, but it cannot read later parameters that are not initialized yet. function f(a = b, b = 1) {} throws when called with no arguments.
Why TDZ exists
TDZ prevents subtle bugs. Instead of silently using undefined, JavaScript tells you that your code is relying on a value before it has been initialized. It also makes block scope and const semantics sound: a const binding should not exist in a usable, unassigned state.
Visual Diagrams
enter block | | binding exists but is uninitialized | reads and writes throw ReferenceError v let value = 10 | | binding initialized v value can be read and written according to declaration rules | leave block
The binding exists before initialization, but it is unusable.
outer value = outer
{
inner value binding created at block entry
console.log(value) hits inner TDZ, not outer
let value = inner
}The inner lexical binding shadows the outer name for the whole block.
Code Examples
Shadowing creates a TDZ trap
The inner let user shadows the outer user from the start of the block.
typeof is not always safe
A truly undeclared name is safe with typeof, but a TDZ binding is not.
Playground
Press Run to execute the code and see output here.
Output Prediction
Predict the output #1
1let value = 'outer';2 3{4 try {5 console.log(value);6 } catch (error) {7 console.log(error.name);8 }9 10 let value = 'inner';11 console.log(value);12}13 14console.log(value);Predict the output #2
1console.log(typeof missing);2 3try {4 console.log(typeof token);5} catch (error) {6 console.log(error.name);7}8 9let token = 'abc';10console.log(typeof token);Predict the output #3
1function demo(a = b, b = 2) {2 console.log(a);3 console.log(b);4}5 6try {7 demo();8} catch (error) {9 console.log(error.name);10}11 12function okay(a = 1, b = a + 1) {13 console.log(a);14 console.log(b);15}16 17okay();Coding Exercises
Make configuration initialization TDZ-safe
MediumImplement buildUrl(options) without reading any variable before it is declared. It should default protocol to 'https', default host to 'example.com', default path to '/home', and return protocol + '://' + host + path.
Interview Questions
1What is the temporal dead zone?
The TDZ is the period from entering a scope until a let, const, or class declaration is initialized. The binding exists during that period, but any access throws ReferenceError. This is why lexical declarations are hoisted but not usable before their declaration line executes.
Follow-ups
- Does typeof throw in the TDZ?
- Why does inner let shadow an outer variable before its declaration line?
2Why does `typeof x` sometimes throw?
typeof only safely returns undefined for names that are truly undeclared. If x is a lexical binding in the current scope but is still uninitialized in the TDZ, typeof x triggers the same ReferenceError as a normal read. The name exists; it is just not initialized yet.
Quiz
1. When does the TDZ for a `let` binding end?
2. What does `typeof missingName` return when `missingName` is truly undeclared?
3. Which binding can be read before its declaration line and produce `undefined`?
Summary
- TDZ runs from scope entry until a lexical or class declaration initializes.
- `let`, `const`, and `class` are hoisted as bindings but cannot be accessed in the TDZ.
- An inner lexical binding shadows outer bindings even before its declaration line.
- `typeof` is safe for undeclared names but throws for TDZ bindings.
Cheat Sheet
TDZ applies to: let, const, class (and many module-binding scenarios).
Starts: when the scope is entered.
Ends: when execution initializes the declaration.
Early access: ReferenceError, including typeof for TDZ bindings.
Shadowing: inner lexical binding wins for the whole block, even before its declaration line.