Hoisting and the Temporal Dead Zone
Declarations are 'hoisted' to the top of their scope. `var` is hoisted and initialized to `undefined` (no TDZ). `let`/`const` are hoisted but in a Temporal Dead Zone (TDZ) until the declaration line — reading them throws ReferenceError. Function declarations are fully hoisted (callable before the line); function expressions and arrow functions follow the `var`/`let`/`const` rules of whatever declares them.
Hoisting and the Temporal Dead Zone are the rules for what's accessible when in a scope. Getting them wrong gives you ReferenceErrors and surprising undefineds.
var — hoisted, initialized to undefined
console.log(x); // undefined (not ReferenceError)
var x = 5;
console.log(x); // 5The declaration is hoisted; the assignment stays put. So x exists from the top of its function scope, holding undefined until the assignment runs.
let / const — hoisted but in TDZ
console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 5;let and const are also hoisted — but reading them before the declaration line throws. The window from "scope start" to "declaration line" is the Temporal Dead Zone (TDZ).
{
// y is in TDZ here
console.log(typeof y); // ReferenceError (not "undefined")
let y = 1;
console.log(y); // 1
}Even typeof throws in TDZ — unlike for undeclared variables, where typeof returns "undefined".
Function declarations — fully hoisted
greet(); // works!
function greet() { return "hi"; }Both the name and the function body are hoisted. Callable before the line.
Function expressions / arrow functions — follow the declarator
foo(); // TypeError: foo is not a function (foo is undefined)
var foo = function() {};
bar(); // ReferenceError: Cannot access 'bar' before initialization
const bar = () => {};The variable is hoisted per its declarator's rules (var: undefined; let/const: TDZ). The function value isn't assigned until the line runs.
class — also in TDZ
new Foo(); // ReferenceError
class Foo {}class declarations are hoisted but in TDZ until the declaration.
Block scope vs function scope
varis function-scoped — leaks out ofif/forblocks.let/constare block-scoped — confined to the nearest{}.
if (true) { var a = 1; let b = 2; }
console.log(a); // 1 — var leaked
console.log(b); // ReferenceError — let scoped to blockWhy TDZ exists
Without it, let would behave like var and you'd have undefined reads before the declaration. TDZ makes the temporal ordering explicit — you can only use the binding after you've declared it. Earlier reads are bugs; the error makes them surface.
Common bugs
Loop var capture
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 3, 3, 3
}var i is shared. Fix with let i (fresh binding per iteration).
Function declaration in a block
if (cond) {
function fn() {} // behavior varies by mode/host — avoid
}Use function expressions inside blocks.
TDZ trap with class fields
Default parameter referencing later-declared const:
function f(a = b, b = 1) {}
f(); // ReferenceError on 'b' in TDZParameters are processed left-to-right.
Modern JavaScript style
Prefer const; let when reassignment needed; avoid var. Modern code rarely has hoisting surprises because:
const/letare block-scoped.- TDZ surfaces order errors immediately.
- ESLint rules (
no-use-before-define) catch the rest.
Interview framing
"Declarations are hoisted but initialization isn't. var is hoisted and initialized to undefined — function-scoped, no TDZ. let and const are hoisted but live in the Temporal Dead Zone until their declaration line — reading them there throws ReferenceError (and even typeof throws, unlike for undeclared identifiers). Function declarations are fully hoisted, name and body. Function expressions and arrow functions follow the rules of whatever declares them — var gives undefined, let/const give TDZ. class is also TDZ. Modern style: prefer const, then let, avoid var — TDZ surfaces ordering bugs at the line that's wrong instead of leaving undefined to ripple through."
Follow-up questions
- •Why doesn't typeof on a let in TDZ return 'undefined'?
- •Explain the var-in-loop closure puzzle.
- •What's the difference in function vs block scope?
- •When is a function declaration callable?
Common mistakes
- •Assuming let/const aren't hoisted (they are; they just TDZ).
- •Using var and hitting scope leaks.
- •Function declaration inside a block.
- •Default-parameter that references a later-declared parameter.
Performance considerations
- •Negligible — engines handle hoisting transparently.
Edge cases
- •typeof in TDZ — throws.
- •Class declarations in TDZ.
- •let/const with the same name re-declared in nested block — shadow, not error.
Real-world examples
- •Migrating var-heavy code to let/const — surfacing latent ordering bugs.
- •Lint rules `no-var` and `no-use-before-define`.