What is temporal dead zone (TDZ) in JavaScript
Temporal Dead Zone (TDZ) is the period from the start of a block scope to the line where let or const is initialized. Accessing the variable during this period throws a ReferenceError.
Theory
TL;DR
- TDZ is like a shelf marked "reserved": the slot exists in memory, but reading it throws an error.
varinitializes toundefinedon hoist.let/conststay uninitialized until the assignment line.- Accessing
let/constbefore that line givesReferenceError, notundefined. - Use
let/constin all modern code. TDZ turns silent bugs into explicit errors.
Quick example
console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 5;
console.log(a); // 5
console.log(b); // undefined - no TDZ for var
var b = 5;Both variables are hoisted. But var b gets undefined immediately, while let a stays uninitialized until line 2 runs.
Key difference
var hoists and auto-initializes to undefined, so a pre-declaration read returns a value without error. let and const hoist only the binding into the lexical environment, leaving it uninitialized. Any read before the assignment line triggers a ReferenceError. The TDZ ends the moment the assignment executes.
When to use
- Block-scoped variable that changes value →
let. - Value that should stay fixed →
const(TDZ plus no reassignment). - Old
forloop in a pre-ES6 library →var, no other reason. - React hooks, Express handlers, module imports →
constby default.
Comparison table
| Variable | Hoisted | Initialized on hoist | TDZ | Pre-init access |
|---|---|---|---|---|
var | Yes | undefined | No | Returns undefined |
let | Yes | No | Yes | ReferenceError |
const | Yes | No | Yes | ReferenceError |
Classes declared with class follow the same rule as let. Accessing the class before its declaration line throws.
How the engine handles this
V8 runs two passes. The first hoists var declarations and sets them to undefined. For let/const, it creates an uninitialized binding in the lexical environment record. The second pass executes line by line. A read on an uninitialized binding throws ReferenceError. That is all TDZ is at the engine level.
Common mistakes
Assuming let is not hoisted at all:
let x;
console.log(x); // undefined - declared but not yet assigned
x = 10;
console.log(x); // 10let is hoisted. It just stays uninitialized. That is why accessing it before the declaration line gives "cannot access before initialization", not "x is not defined".
Destructuring params without defaults:
function fn({ a }) {
console.log(a);
}
fn(); // ReferenceError - destructured binding has its own TDZ
fn({}); // undefined - fineFix: function fn({ a = 1 }).
Confusing ReferenceError with TypeError:
const x = 1;
x = 2; // TypeError, not ReferenceErrorReferenceError happens during TDZ, before init. TypeError happens after TDZ ends, when you try to reassign a const. Different errors, different causes.
Most TDZ bugs I've seen in code review come from refactoring var to let without checking whether the variable is read before its declaration in the same block.
Real-world usage
- React:
const [state, setState] = useState(null)- TDZ prevents reads before the hook line. - Express:
const id = req.params.idinside a route handler - catches accidental pre-parse access. - Node.js modules:
const fs = require('fs')at top level - strict mode enforces TDZ throughout. - Redux:
const action = { type: 'FETCH_USER' }- preventsundefinedaction types from reaching reducers.
Follow-up questions
Q: What is the output of console.log(x); let x = 5;?
A: ReferenceError: Cannot access 'x' before initialization. The variable is hoisted but stays in TDZ until the assignment runs.
Q: Does const have a TDZ?
A: Yes, same rules as let. TDZ ends at the assignment line. After that, trying to reassign throws TypeError, not ReferenceError.
Q: Why does TDZ exist?
A: It makes early variable access a loud, explicit error instead of returning undefined without error. With var, pre-declaration reads return undefined with no warning, which can cause bugs that are hard to trace.
Q: How does Babel simulate TDZ when transpiling to ES5?
A: Babel uses a void 0 sentinel check to approximate the uninitialized state. It gets close to the spec behavior, but edge cases can differ from native engine handling.
Examples
Basic TDZ behavior
// Both variables are hoisted from the start of the scope.
// var: initialized to undefined immediately.
// let: stays uninitialized until the declaration line runs.
console.log(name); // undefined (var - no TDZ)
var name = 'Alice';
console.log(age); // ReferenceError: Cannot access 'age' before initialization
let age = 30;name returns undefined because var initializes on hoist. age throws because let leaves its binding uninitialized until line 9.
TDZ in a request handler
// Safe pattern - access happens after declaration
function handleLogin(userId) {
if (!userId) throw new Error('Missing user ID');
const token = generateToken(userId); // TDZ has passed by this line
return token;
}
// Unsafe pattern - TDZ violation
function badHandler(userId) {
console.log(token); // ReferenceError - token is in TDZ here
const token = generateToken(userId);
return token;
}In handleLogin, token is only read after its declaration. In badHandler, reading token before the const line throws immediately. TDZ surfaces this in development, not in production.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.