Hoisting & the TDZ
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
| Declaration | Name exists early? | Value before its line |
|---|---|---|
var | Yes | undefined |
| function declaration | Yes | The full function, ready to call |
let / const | Yes, but locked | ReferenceError |
function expression (const f = () => {}) | Follows its variable rules | Same as its variable |
class | Yes, but locked | ReferenceError |
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.
1{2 // TDZ for x starts here3 console.log(x); // ReferenceError: Cannot access 'x' before initialization4 let x = 10; // TDZ ends here5 console.log(x); // 106}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.
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.
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
functionor place them above where they are used. Arrow functions assigned toconstmust come before the code that calls them runs. - Components are usually
functiondeclarations orconstarrows. Withconst, you cannot render the component above its definition at the top level. - A hook or variable referenced in a
useEffectcallback is fine even if declared below, because the callback runs later, after the whole function body has finished.
1function Page() {2 useEffect(() => {3 show(); // ✓ runs after render, show already exists4 }, []);5 6 const show = () => console.log("ready");7 return null;8}Summary
1Hoisting → declarations are registered before code runs2var → hoisted, starts as undefined3function declaration → hoisted fully, callable early4let / const / class → hoisted but locked in the TDZ until their line5TDZ → the gap where touching the name throws a ReferenceError6Function expression → only as early as its variable allowsHoisting and scope together lead to the most important idea in the section: a function that remembers the variables around it. That is a closure.