What is hoisting?
Declarations move up. Initializations don't.
JavaScript "hoists" declarations to the top of their scope before running the code. That's why you can call a function before it appears in the file. But hoisting has limits — var hoists to undefined, let/const hoist to a temporal dead zone that throws if you access them early, and function expressions don't hoist their values at all. This page covers each case with examples.
Short answer: Function declarations hoist completely — you can call them before they appear. var hoists the declaration but not the value, so accessing before assignment returns undefined. let/const hoist but stay in the temporal dead zone — accessing before declaration throws ReferenceError. Function expressions don't hoist their value (the variable hoists, the function doesn't). The best practice: declare before use. Hoisting is convenient, not a reason to rely on it.
✅ The three cases
Each keyword hoists differently.
Function declarations
Hoisted with their entire body. Callable before they appear in the source.
var
Hoisted to undefined. Accessing before assignment returns undefined (no error).
let / const
Hoisted into a temporal dead zone. Accessing before declaration throws ReferenceError.
Function expressions
The variable hoists, the function doesn't. const fn = () => {} is not callable before the line.
✅ Before and after
What hoists and what doesn't.
// ✅ Function declaration — hoisted
sayHi();
function sayHi() {
console.log('Hi');
}
// Works. Function is available before the line.
// ⚠️ var — hoisted to undefined
console.log(x); // undefined (not error)
var x = 5;
console.log(x); // 5
// ❌ let / const — temporal dead zone
console.log(y); // ReferenceError
let y = 5;
// ❌ Function expression — not hoisted
greet(); // TypeError: greet is not a function
const greet = () => console.log('Hello');
Actual output: sayHi() → "Hi" (function declaration hoisted) console.log(x) → undefined (var hoisted as undefined) console.log(x) → 5 (after assignment) console.log(y) → ReferenceError (TDZ, let not initialized) greet() → TypeError: greet is not a function (const hoisted, but the value is undefined until the line executes — calling undefined throws) Key insight: Hoisting moves declarations up. It does NOT move initializations up. A function declaration has both declaration and body hoisted (that's why it works). A function expression only hoists the variable — the function is assigned at execution time. The TDZ is the modern safety net: let/const throw when accessed early. var returns undefined (silent bug). The TDZ catches bugs that var hides.
✅ The temporal dead zone
What let and const add to hoisting.
// The temporal dead zone starts at the top of the
// scope and ends at the declaration.
function example() {
// TDZ for x starts here
console.log(x); // ReferenceError
let x = 5; // TDZ ends here
console.log(x); // 5
}
// With var — no TDZ
function example2() {
console.log(y); // undefined
var y = 5;
console.log(y); // 5
}
// Why the difference:
// var is initialized to undefined at scope entry.
// let/const are declared at scope entry, but
// initialized at their declaration line.
// Between those two points is the TDZ.
The TDZ is not a bug — it's a feature. Without TDZ (like var): console.log(x); // undefined let x = 5; → Silently returns undefined. User thinks x is undefined, but it's just not initialized yet. → Hides bugs like typos or wrong variable names. With TDZ (let/const): console.log(x); // ReferenceError let x = 5; → Throws loudly. User sees the bug immediately. → Cannot accidentally use an uninitialized value. ✅ TDZ surfaces bugs early. ✅ TDZ catches the "used before declared" mistake. ✅ TDZ forces you to declare variables where you intend to use them. The error message is helpful: "Cannot access 'x' before initialization" This is one of the reasons let/const are recommended over var.
✅ Function declarations vs expressions
The rule that trips people up.
// ✅ Function declaration — hoisted with body
console.log(declared()); // "I'm declared"
function declared() {
return "I'm declared";
}
// ❌ Function expression — NOT hoisted
console.log(expressed()); // TypeError
const expressed = function () {
return "I'm expressed";
};
// ❌ Arrow function assigned to const — NOT hoisted
console.log(arrow()); // TypeError
const arrow = () => "I'm an arrow";
Why the difference:
Function declaration:
function declared() { ... }
→ The entire function is hoisted.
→ Calling it before the line works.
Function expression:
const expressed = function () { ... }
→ Only the variable expressed is hoisted.
→ The variable starts as undefined (TDZ for const).
→ Calling undefined() throws TypeError.
→ The function value is assigned at execution time.
Same for arrow functions assigned to variables:
const arrow = () => ...
→ The variable hoists (TDZ for const).
→ The value is assigned at execution time.
→ Calling before the line throws.
Rule:
Function declarations hoist completely.
Function expressions (any form) do not.
Practical implication:
If you need to call a function before defining it,
use a function declaration. Otherwise, prefer
const + arrow (or function expression) for clarity —
it makes the dependency explicit.
Modern style prefers function expressions for
most cases — the hoisting benefit of declarations
is rarely worth the readability cost of "why is
this function defined at the bottom?"
Don't rely on hoisting as a style. Just because a function declaration works before its definition doesn't mean you should write code that way. Hoisting is a mechanism, not a feature you should design around. If a function is called at the top of a file and defined at the bottom, that's harder to read — the reader has to scroll to understand what the function does. The modern style: declare functions and variables near where they're used, or at the top of the file for clarity. Use function declarations when you have a reason to (mutual recursion, callback ordering), not as a default.
⚠️ Common mistakes
The hoisting mistakes that cost real time.
They don't — the value is undefined until assignment. Fix: declare before use, or use a function declaration.
let/const before declarationTDZ throws a ReferenceError. Fix: declare before use.
var hoistingIt silently returns undefined — hides bugs. Fix: use let/const.
Behavior is engine-dependent in sloppy mode. Fix: don't rely on it; declare outside the block.
Hoisting moves declarations to the top of their existing scope — it doesn't change the scope. Fix: understand scope separately.
Both hoist, so both work. Fix: this is a legitimate use of hoisting.
class declarationsClasses hoist but stay in the TDZ — accessing before declaration throws. Fix: declare classes before use.
Arrow functions don't hoist (same as expressions). Fix: declare before use.
❓ Frequently asked
What is hoisting in JavaScript?
Hoisting is JavaScript's behavior of moving declarations to the top of their scope before code runs. Function declarations are hoisted entirely — you can call them before they're defined. var declarations are hoisted but initialized to undefined. let and const declarations are hoisted but stay in a temporal dead zone — accessing them before declaration throws a ReferenceError.
Why can I call a function before declaring it?
Because function declarations are hoisted with their entire body. When JavaScript parses your code, it registers all function declarations in the current scope before executing any line. This is different from function expressions: const greet = function () { ... } is not hoisted — the variable is hoisted but the value isn't.
What is the temporal dead zone?
The temporal dead zone (TDZ) is the period between entering a scope and the point where a let or const variable is initialized. During the TDZ, the variable is hoisted (it exists in the scope) but accessing it throws a ReferenceError. Example: console.log(x); let x = 5; throws. This is intentional — it catches the bug of using a variable before declaring it.
Is hoisting a bug or a feature?
Both. For function declarations, hoisting is convenient and expected. For var, hoisting is a bug source — it hides typos and undeclared variables. The let/const TDZ is the modern compromise: hoisting still happens (to keep function calls working), but accessing before declaration now throws, which catches bugs early.
Does hoisting apply to functions inside blocks?
Function declarations inside blocks are complex. In strict mode, they're block-scoped — they only exist inside the block. In sloppy mode, behavior varies across engines. The safe rule: don't rely on it. If you need a function only inside a block, use a function declaration and accept whatever the engine does. If you need it outside the block, declare it outside.
How does hoisting interact with closures?
Closures capture variables, not values. If a closure references a variable that's hoisted but not yet initialized (a let/const in the TDZ), and the closure is called before initialization, it throws. With var, the closure sees undefined if called before the assignment. This is why the loop closure bug happens — all callbacks capture the same var.
🔗 Continue learning
Next in this series:
setTimeout(0) doesn't run first