Explain the Temporal Dead Zone (TDZ)
The Short Answer
The Temporal Dead Zone (TDZ) is the period between entering a scope and the point where a let or const variable is declared. During this window, the variable exists (it's been hoisted) but accessing it throws a ReferenceError. Unlike var (which is hoisted and initialized to undefined), let and const are hoisted but NOT initialized — they sit in the TDZ until execution reaches the declaration line. This prevents the confusing behavior where var variables are accessible before their declaration with a value of undefined.
TDZ in Action
The TDZ starts at the beginning of the enclosing block (not the function — the block) and ends when the declaration is evaluated. Any attempt to read or write the variable during this window throws a ReferenceError. This is different from an undeclared variable — the engine knows the variable exists (it's been hoisted), but it refuses to let you use it before initialization.
// var — hoisted AND initialized to undefined (no TDZ)
console.log(x); // undefined (no error)
var x = 10;
console.log(x); // 10
// let — hoisted but NOT initialized (TDZ until declaration)
console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 10;
console.log(y); // 10 (only reachable if above line is removed)
// const — same TDZ behavior as let
console.log(z); // ReferenceError: Cannot access 'z' before initialization
const z = 10;
Notice the error message says "Cannot access before initialization" — not "is not defined". This proves the engine knows the variable exists (it was hoisted), but it's in the TDZ and can't be accessed yet.
TDZ Is Per-Block, Not Per-Function
Since let and const are block-scoped, the TDZ is also block-scoped. Each block ({}) creates its own TDZ for any let/const declarations within it. This means the same variable name can be in the TDZ in one block while being fully accessible in an outer block — they're different variables in different scopes.
let name = 'outer';
{
// TDZ for inner 'name' starts here (top of block)
// Even though outer 'name' exists, this block has its own 'name'
console.log(name); // ReferenceError! Inner 'name' is in TDZ
// You might expect 'outer', but the inner declaration shadows it
let name = 'inner'; // TDZ ends here
console.log(name); // 'inner'
}
console.log(name); // 'outer' (outer variable unaffected)
// Each iteration of a for loop creates a new block scope
for (let i = 0; i < 3; i++) {
// Fresh TDZ for 'i' in each iteration — but it's immediately initialized
// so you never actually hit the TDZ in normal for-loop usage
console.log(i); // 0, 1, 2
}
Subtle TDZ Scenarios
The TDZ catches some non-obvious cases. Default parameter values can trigger TDZ errors if they reference later parameters. Closures that capture TDZ variables don't throw until the closure is actually called (because the variable might be initialized by then). And typeof — which normally never throws — DOES throw for TDZ variables.
// typeof throws in TDZ (unlike undeclared variables!)
typeof undeclaredVar; // 'undefined' (no error for undeclared)
typeof tdzVariable; // ReferenceError! (TDZ)
let tdzVariable = 1;
// Default parameters — left-to-right TDZ
function broken(a = b, b = 1) {
// 'a' defaults to 'b', but 'b' is in TDZ when 'a' is evaluated
return [a, b];
}
broken(); // ReferenceError: Cannot access 'b' before initialization
function working(a = 1, b = a) {
// 'b' defaults to 'a', and 'a' is already initialized
return [a, b];
}
working(); // [1, 1] ✓
// Closures and TDZ — the closure captures the binding, not the value
let callback;
{
callback = () => console.log(value); // captures 'value' binding
// If we call callback() HERE → ReferenceError (value in TDZ)
let value = 42;
callback(); // 42 ✓ (called AFTER initialization)
}
// Class declarations also have TDZ
const instance = new MyClass(); // ReferenceError
class MyClass {}
Why TDZ Exists
The TDZ was introduced with ES6 to fix a long-standing source of bugs with var. When var is hoisted and initialized to undefined, you can accidentally use a variable before assigning it and get undefined instead of an error — a silent bug that's hard to track down. The TDZ makes this a loud, immediate error. It also makes const semantically correct — if const were initialized to undefined before its declaration, it would technically be "reassigned" when the declaration runs, violating the const guarantee.
| Behavior | var | let / const |
|---|---|---|
| Hoisted? | Yes | Yes |
| Initialized on hoist? | Yes (to undefined) | No (stays in TDZ) |
| Access before declaration | Returns undefined | ReferenceError |
| Scope | Function | Block |
| typeof before declaration | "undefined" | ReferenceError |
Why Interviewers Ask This
This question tests deep understanding of JavaScript's variable lifecycle. Interviewers want to see that you know let/const ARE hoisted (many candidates incorrectly say they aren't), understand the difference between hoisting and initialization, can explain why TDZ exists (catching bugs, making const semantically sound), and know the edge cases (typeof throwing, default parameter ordering, class declarations). It separates developers who memorized "use let/const" from those who understand the mechanics.
Quick Revision Cheat Sheet
What is TDZ: The gap between scope entry and variable declaration where access throws
Applies to: let, const, class declarations (NOT var or function declarations)
Error message: "Cannot access 'x' before initialization" (proves it IS hoisted)
typeof in TDZ: Throws ReferenceError (unlike undeclared variables)
Why it exists: Catches use-before-declare bugs that var silently allows
Scope: Per-block — each {} has its own TDZ for its let/const declarations