Back to JavaScript
JavaScript
easy
mid

What is hoisting in JavaScript and what is 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.

4 min read·~10 min to think through

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

js
console.log(x);   // undefined (not ReferenceError)
var x = 5;
console.log(x);   // 5

The 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

js
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).

js
{
  // 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

js
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

js
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

js
new Foo();     // ReferenceError
class Foo {}

class declarations are hoisted but in TDZ until the declaration.

Block scope vs function scope

  • var is function-scoped — leaks out of if/for blocks.
  • let/const are block-scoped — confined to the nearest {}.
js
if (true) { var a = 1; let b = 2; }
console.log(a);    // 1 — var leaked
console.log(b);    // ReferenceError — let scoped to block

Why 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

js
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

js
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:

js
function f(a = b, b = 1) {}
f();   // ReferenceError on 'b' in TDZ

Parameters are processed left-to-right.

Modern JavaScript style

Prefer const; let when reassignment needed; avoid var. Modern code rarely has hoisting surprises because:

  • const/let are 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`.

Senior engineer discussion

Seniors prefer const, use let where needed, avoid var, and articulate TDZ as a deliberate design choice — not a quirk.

Related questions