Problem Solved

JavaScript · hoisting

Home › Courses › JavaScript › Problems › Hoisting
🟨 Concept · 3 min read

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.

hoisting-cases.js
// ✅ 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.

tdz.js
// 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-hoisting.js
// ✅ 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.

1. Expecting function expressions to hoist

They don't — the value is undefined until assignment. Fix: declare before use, or use a function declaration.

2. Accessing let/const before declaration

TDZ throws a ReferenceError. Fix: declare before use.

3. Relying on var hoisting

It silently returns undefined — hides bugs. Fix: use let/const.

4. Function declaration inside a block

Behavior is engine-dependent in sloppy mode. Fix: don't rely on it; declare outside the block.

5. Confusing hoisting with scope

Hoisting moves declarations to the top of their existing scope — it doesn't change the scope. Fix: understand scope separately.

6. Mutually recursive function declarations

Both hoist, so both work. Fix: this is a legitimate use of hoisting.

7. Hoisting class declarations

Classes hoist but stay in the TDZ — accessing before declaration throws. Fix: declare classes before use.

8. Hoisting in arrow functions

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

🗺️ JavaScript learning path
The full roadmap from variables to async — every concept in order

Next in this series:

⏳ How does the event loop work?
Microtasks, macrotasks, and why setTimeout(0) doesn't run first
← All JavaScript problems
Problem solved · JavaScript