Temporal Dead Zone
Why let and const throw if you touch them early — binding creation vs initialization, TDZ edges with defaults, typeof, and closures.
- javascript
- tdz
- let
- const
- scope
The Temporal Dead Zone (TDZ) is the stretch of time from when a scope’s let/const/class binding is created until it is initialized by evaluating its declaration. Access the binding in that window and you get ReferenceError — not undefined.
That is not “let isn’t hoisted.” The binding is registered at the start of the block. It is intentionally unusable until the initializer runs. The “temporal” part is about execution time inside the block, not calendar time.
If you only contrast “var is hoisted, let isn’t,” you’ll mis-explain half the interview questions and misread stack traces that say “before initialization.”
The problem TDZ fixes
With var, reading early is silent:
function legacy() {
console.log(config); // undefined — bug hides until something assumes a real value
var config = { theme: 'dark' };
}
let/const force the bug into the open:
function modern() {
console.log(config); // ReferenceError: Cannot access 'config' before initialization
const config = { theme: 'dark' };
}
TDZ is a correctness feature: no accidental use of a half-declared binding.
Model: create → dead zone → initialize → live
{
// Binding for `x` exists here (block starts) — still in TDZ
// console.log(x); // ReferenceError
let x = 1; // initialization ends TDZ for `x`
console.log(x); // 1
}
Mental sequence for a block:
- Enter block → create
let/constbindings, mark uninitialized. - Run statements top to bottom.
- Hit
let x = expr→ evaluateexpr, store value, mark initialized. - Later reads/writes use the live binding.
const is the same for TDZ, then forbids reassignment after init. TDZ and immutability are separate rules.
Classic demos interviewers use
Shadowing an outer binding
const value = 'outer';
{
// console.log(value); // ReferenceError — inner `value` is in TDZ, not outer
const value = 'inner';
console.log(value); // inner
}
The inner declaration shadows for the whole block, including lines above it. The outer value is not “visible until the inner line.” That surprises people who think of TDZ as only “the one line.”
typeof is not a free pass
console.log(typeof undeclared); // "undefined" — no binding
{
// console.log(typeof x); // ReferenceError — binding exists, still TDZ
let x = 1;
}
typeof only soft-fails for truly undeclared identifiers (in non-module / non-strict edge discussions). A TDZ binding is declared; typeof still throws.
Functions that close over TDZ bindings
{
const fn = () => n;
// fn(); // would throw if called before init
let n = 5;
console.log(fn()); // 5 — called after init; closure sees live binding
}
Creating a function that mentions n is fine during TDZ. Calling it before initialization is not. Closures capture bindings, not snapshots of “was it ready.”
See Closures.
Default parameters
Parameter defaults are evaluated left-to-right in a special environment. Later parameters can see earlier ones; earlier cannot see later ones:
function pair(a = b, b = 2) {
return [a, b];
}
// pair(); // ReferenceError when evaluating default for `a`
function pairOk(a = 1, b = a + 1) {
return [a, b];
}
pairOk(); // [1, 2]
pairOk(5); // [5, 6]
Treat parameter list order like a mini TDZ scope. Reorder or compute defaults in the body when dependencies get messy.
class and circular const
// const c = new Circle(1); // ReferenceError
class Circle {
constructor(r) {
this.r = r;
}
}
const a = b; // ReferenceError if b is const below in same scope
const b = 1;
Mutual const references in the same scope need care — one binding’s initializer cannot read the other while still dead.
var has no TDZ (and that is the bug)
function f() {
console.log(x); // undefined
var x = 3;
}
There is no uninitialized state for var after instantiation. You trade safety for historical behavior. New code: const by default, let when reassignment is real.
Full declaration comparison: Hoisting in JavaScript, var vs let vs const.
Where TDZ shows up in product code
Temporal dependency between hooks setup and constants
function Panel() {
// Bad pattern in plain JS modules too:
// useSomething(DEFAULT); // if DEFAULT is const below in same block — TDZ
const DEFAULT = { open: false };
// ...
}
Declare constants above first use. Linters and humans both prefer it; TDZ enforces it for let/const.
Switch / case without blocks
switch (tag) {
case 'a':
let msg = 'A'; // TDZ / redeclare issues if another case also lets `msg`
break;
case 'b':
// let msg = 'B'; // SyntaxError: already declared in switch block
break;
}
switch shares one block. Wrap cases in { } when you need case-local let/const.
switch (tag) {
case 'a': {
let msg = 'A';
break;
}
case 'b': {
let msg = 'B';
break;
}
}
Redeclaration
let x = 1;
// let x = 2; // SyntaxError in same scope
TDZ is about initialization time. Redeclaration is a separate parse-time error. Both push you toward clearer scopes.
Footguns
- Saying “let is not hoisted” — wrong. Binding is created; access is gated.
- Assuming outer variables are visible above an inner
letof the same name — shadowing is whole-block. - Calling a closure early that closes over a
conststill in TDZ. - Default parameter order creating silent design debt when someone reorders args.
- Mixing
varandletfor the same mental model — they are different languages’ worth of behavior.
Interview angle
Prompt: “What is the temporal dead zone?”
Strong answer: “From the start of a block until a let/const/class declaration is evaluated, the binding exists but is uninitialized. Reading or writing it throws ReferenceError. That’s different from var, which initializes to undefined at scope entry. Shadowing means an inner binding is in TDZ for the whole block, so even an outer same-named variable isn’t readable above the inner declaration.”
Live code prediction:
let x = 1;
{
// console.log(x);
let x = 2;
}
Explain why the commented line would throw (inner TDZ), not print 1.
Connect to hoisting without collapsing the two ideas.
Related on this site
- Hoisting in JavaScript — instantiation vs evaluation
- var vs let vs const — choosing declarations
- Closures — live bindings and loops
- Strict Mode — fewer silent failures
- this Binding Rules — different axis of “surprising access”
Further reading
- Temporal dead zone — MDN (
let) - const — MDN
- javascript.info — The old “var”
- ECMAScript: CreateMutableBinding / InitializeBinding
Related guides
- Hoisting in JavaScriptWhat the engine does before your code runs — var vs function vs let/const, the TDZ, and the bugs that look like magic until you name the phase.
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.