let, const, var, and the Temporal Dead Zone
let and const are block-scoped and unreachable until their line runs, while var is function-scoped and available as undefined from the top.
Loading visual…
Definition
`var` declarations are function-scoped (or global-scoped) and hoisted with an initial value of `undefined`, so a `var` variable exists and reads as `undefined` from the very top of its scope, even before its declaration line runs. This makes `var` easy to misuse: it ignores block boundaries like `if` and `for`, so the same name can leak out of a loop or conditional. `let` and `const` are block-scoped, confined to the nearest `{}`. They are registered ahead of time like `var`, but they stay in the temporal dead zone (TDZ) from the start of the block until their declaration line actually executes. Accessing a `let`/`const` variable inside its TDZ throws a `ReferenceError` instead of silently returning `undefined`. `const` additionally forbids reassigning the binding after it is set (though the value it points to, if an object, can still be mutated).
Examples
console.log(count); // ReferenceError: Cannot access 'count' before initialization
let count = 5;
console.log(count); // 5count exists as soon as the block starts, but it stays in the temporal dead zone until its declaration line runs, so reading it earlier throws instead of returning undefined.
console.log(msg); // undefined (no error)
var msg = "hi";
console.log(msg); // "hi"var is hoisted and initialized to undefined immediately, so reading it before its assignment line is legal but returns undefined rather than the eventual value.
for (var i = 0; i < 3; i++) {}
console.log(i); // 3, i leaked out of the loop
for (let j = 0; j < 3; j++) {}
console.log(j); // ReferenceError, j is block-scoped to the loopvar ignores the for loop's block boundary and leaks into the enclosing scope, while let is confined to the loop body.
Where you see this
- Choosing let/const by default so a typo or early read throws immediately instead of silently producing undefined.
- Avoiding the classic var-in-a-loop bug where every closure captures the same leaked variable.
- Reasoning about a 'Cannot access before initialization' error, which points at a TDZ violation, not a missing declaration.
- Using const to signal a binding will never be reassigned, catching accidental reassignment at parse time.