Course outline · 0% complete

0/27 lessons0%

Course overview →

Hoisting and the temporal dead zone

lesson 10-1 · ~11 min · 25/27

var gives the whole loop one shared i, and every closure sees its final value.

Closures capture the variable rather than a snapshot, and var creates a single function-wide binding, so all three arrows read the same slot.

The value is 3 rather than 2 because the loop only exits after the increment makes the condition fail.

Switching to let fixes it by giving each iteration its own binding, which is the whole difference between the two keywords in a loop.

This unit is a gauntlet of exactly these classics, and the goal is a precise explanation rather than a memorized answer.

First up is where declarations really live, which is the mechanism that made the var version behave that way in the first place.

Hoisting: declarations move up

Before running a scope, JavaScript registers every declaration in it first. What differs is the initial value.

DeclarationHoisted asReading it early
var x = 10name only, set to undefinedgives undefined
function greet() {}the whole functionworks, callable above
let / constregistered, uninitializedthrows ReferenceError

The assignment part of var x = 10 stays exactly where you wrote it, and only the name moves.

The region above a let or const line is called the temporal dead zone, or TDZ, and it lasts from the start of the block to the declaration.

This is not trivia. Millions of lines of pre-2015 code use var, and reading legacy code means predicting it.

Hoisting also explains why let and const were designed with the TDZ. It turns a silent undefined bug into a loud, findable error.

Two smaller notes complete the picture. Class declarations are in the TDZ like let, and function expressions assigned to a variable hoist only the variable, so greet() above const greet = () => {} throws.

var score = 10name exists: undefinedvalue assigned: 10let score = 10temporal dead zonevalue assigned: 10the declaration lineReading early: var gives a silent undefined, let throws a ReferenceError.
Before the declaration line, a var already holds undefined while a let sits unreadable in the dead zone.

Reading a var before its line

Two declarations, two different early behaviors.

console.log(score);
var score = 10;
console.log(score);

greet();
function greet() {
  console.log("hi from below");
}

Output

undefined
10
hi from below

The var read gives undefined, since the name exists and the value has not been assigned yet.

The function call works from above its own definition, because a function declaration hoists whole rather than name-only.

Nothing throws on the first line, which is the dangerous part. A typo'd variable name would throw a ReferenceError, and a hoisted var gives a plausible-looking undefined instead.

The undefined then propagates. Arithmetic on it produces NaN and property access on it throws several lines later, far from the actual mistake.

Function hoisting is a genuine convenience, and it is why older codebases often put helper functions at the bottom of a file. Modern code tends to prefer defining before use anyway, since const arrow functions do not hoist that way.

It throws a ReferenceError, because score is in the temporal dead zone.

let is hoisted but left uninitialized, so any read before the declaration line throws rather than producing a value.

The error message is specific and helpful, saying that score cannot be accessed before initialization, which distinguishes it from a plain undefined-variable error.

That safety is exactly why modern code uses let and const. Undefined leaking out of hoisted vars caused decades of quiet bugs.

typeof does not protect you here either. typeof score returns "undefined" for a name that was never declared, and it throws inside a TDZ.

const behaves identically, and it adds one more rule. It must be initialized on its declaration line, so const x; is a syntax error rather than a deferred assignment.

Catching the TDZ error

The try/catch is here only to show which error appears.

try {
  console.log(score);
  let score = 10;
} catch (err) {
  console.log(err.constructor.name);
}

Output

ReferenceError

Reading score before its let line is inside the temporal dead zone and throws.

Swap let for var and the loud error becomes a silent undefined, which is the comparison this example exists to make.

The let is scoped to the try block rather than the whole function, since let is block-scoped. That is a second difference from var in the same three lines.

err.constructor.name is a compact way to print an error's type, and it works because every error object's prototype chain leads to a named constructor.

The declaration is never reached at all here, and the binding still existed in an unusable state, which is the clearest possible statement of what hoisting-without-initialization means.

The classic hoisting trick

One word of output.

var x = 1;
function f() {
  console.log(x);
  var x = 2;
}
f();

Output

undefined

The var x = 2 hoists its declaration to the top of f, so the whole function body sees a local x.

At the console.log line that local exists and is still undefined, since only the name moved up and the assignment stayed put.

The outer x = 1 is shadowed for the entire function body, including the lines above the assignment, so it is never consulted.

That last point is what makes this a trick question. The shadowing is not positional, and moving the console.log above every statement in the function changes nothing.

Replacing var x = 2 with let x = 2 changes the answer to a ReferenceError, which is the TDZ giving the honest signal that the code is confused.

Deleting the inner declaration entirely prints 1, since the lookup then walks out to the enclosing scope by lesson 1-1's rules.