On this page

Hoisting & the TDZ

3 min read

In the last topic we saw that var, let and const live in different scopes. Now we look at when they come alive. This explains why some variables are undefined and others crash.

Definition

Hoisting is JavaScript's habit of processing declarations before it runs any code. Before a function starts, the engine scans it, finds every declaration, and sets up the names in memory. Then it runs line by line.

Nothing physically moves. It only feels like the declarations were lifted to the top.

Analogy: a teacher takes attendance before the class starts. Every student's name is on the list from the beginning, but some students are marked "present" and ready, and others are marked "arrived but not allowed to speak yet".

How Each Declaration Behaves

DeclarationName exists early?Value before its line
varYesundefined
function declarationYesThe full function, ready to call
let / constYes, but lockedReferenceError
function expression (const f = () => {})Follows its variable rulesSame as its variable
classYes, but lockedReferenceError
js
1console.log(a); // undefined (var is hoisted and set to undefined)
2var a = 5;
3
4sayHi(); // "hi" (function declarations are fully hoisted)
5function sayHi() { console.log("hi"); }
6
7console.log(b); // ReferenceError ✗
8let b = 5;

The Temporal Dead Zone (TDZ)

let and const are hoisted too, but they are not initialized. From the start of the block until the line that declares them, they sit in the temporal dead zone. Touching them in that window throws an error.

js
1{
2 // TDZ for x starts here
3 console.log(x); // ReferenceError: Cannot access 'x' before initialization
4 let x = 10; // TDZ ends here
5 console.log(x); // 10
6}

If let were not hoisted at all, the first console.log(x) would find an outer x instead. The error proves the inner x already exists.

js
1let x = "outer";
2{
3 console.log(x); // ReferenceError, not "outer"
4 let x = "inner";
5}

Why the TDZ Exists

var giving undefined hides bugs. You read a variable too early, get undefined, and the program keeps going with bad data. The TDZ turns that silent bug into a loud error, so you find it right away.

Function Declaration vs Function Expression

This is the favorite interview trick.

js
1greet(); // works ✓
2function greet() { console.log("hello"); }
3
4hello(); // TypeError: hello is not a function ✗
5var hello = function () { console.log("hello"); };

Here is what breaks in the second case. var hello is hoisted as undefined. Calling undefined() gives a TypeError. If it were const hello = ..., you would get a ReferenceError from the TDZ instead.

In React

  • Define helper functions with function or place them above where they are used. Arrow functions assigned to const must come before the code that calls them runs.
  • Components are usually function declarations or const arrows. With const, you cannot render the component above its definition at the top level.
  • A hook or variable referenced in a useEffect callback is fine even if declared below, because the callback runs later, after the whole function body has finished.
jsx
1function Page() {
2 useEffect(() => {
3 show(); // ✓ runs after render, show already exists
4 }, []);
5
6 const show = () => console.log("ready");
7 return null;
8}

Summary

1Hoisting → declarations are registered before code runs
2var → hoisted, starts as undefined
3function declaration → hoisted fully, callable early
4let / const / class → hoisted but locked in the TDZ until their line
5TDZ → the gap where touching the name throws a ReferenceError
6Function expression → only as early as its variable allows

Hoisting and scope together lead to the most important idea in the section: a function that remembers the variables around it. That is a closure.

Next TopicClosures & Lexical Scope