A modern, developer-friendly guide to one of JavaScript’s oldest quirks — explained with real-world examples.

Introduction
Let’s be honest: JavaScript hoisting is one of those things every dev learns once… and then forgets until it bites them again.
Maybe you’ve seen this before:
console.log(x); // undefined
var x = 10;
Why doesn’t that throw an error?
Or this:
console.log(y); // ❌ ReferenceError
let y = 20;
Wait, why is y
different from x
? Aren’t both variables?
Hoisting has been around since JavaScript’s earliest days, but ES6 (2015) and beyond introduced new twists with let
, const
, and modern function declarations. Fast forward to 2025, and devs still ask the same question: What exactly gets hoisted, and how should I think about it?
In this post, we’ll demystify hoisting step by step — no jargon, no hand-waving. By the end, you’ll have a clear mental model you can apply in real projects.
1. What Is Hoisting, Really?
At its core: Hoisting is JavaScript’s way of moving declarations to the top of their scope during the compilation phase.
- Declarations (like
var
,function
) are processed before any code runs. - This means you can sometimes use a variable or function before it’s written in the code.
But the tricky part? Not everything gets hoisted the same way.
👉 Think of it like this:
JavaScript makes a shopping list of variables/functions before running your code. But for some items, it fills in a placeholder (undefined
), while for others, it refuses to give you anything until it’s ready.
2. var Hoisting: The OG Behavior
Classic var
declarations are hoisted to the top of their function scope and initialized with undefined
.
console.log(a); // undefined
var a = 5;
console.log(a); // 5
Why? Because internally, the engine treats your code like this:
var a; // declaration hoisted
console.log(a); // undefined
a = 5; // assignment stays in place
console.log(a); // 5
👉 Real-world impact: Easy to accidentally overwrite values or create “ghost” variables if you forget you declared them later.
3. let & const: The Temporal Dead Zone (TDZ)
let
and const
are also hoisted — but without initialization. Until the declaration line runs, the variable is in the Temporal Dead Zone (TDZ).
console.log(b); // ❌ ReferenceError
let b = 10;
Behind the scenes:
- Declaration is hoisted, but no default
undefined
. - Accessing it before initialization triggers an error.
👉 Why TDZ matters: It prevents bugs caused by using variables before they’re ready.
4. Function Declarations vs Function Expressions
Here’s where confusion skyrockets.
Function Declaration → Fully Hoisted
sayHi(); // ✅ Works
function sayHi() {
console.log("Hello!");
}
The entire function is hoisted — name and body.
Function Expression → Not Hoisted
sayBye(); // ❌ ReferenceError
var sayBye = function () {
console.log("Bye!");
};
Only the var
is hoisted (initialized as undefined
), not the function body.
Arrow Functions (with let/const) → TDZ Rules
sayArrow(); // ❌ ReferenceError
const sayArrow = () => console.log("Arrow!");
👉 Rule of thumb: If it’s a declaration (function foo() {}
), it’s hoisted. If it’s assigned to a variable (const foo = () => {}
), it follows variable hoisting rules.
5. Class Hoisting (Bonus for 2025 Devs)
Classes behave like let
and const
: they are hoisted but live in the TDZ.
const c = new Car(); // ❌ ReferenceError
class Car {}
👉 Gotcha: Unlike functions, classes must be defined before use.
6. Real-World Example: Debugging a Hoisting Bug
Imagine you’re writing a Node.js config loader:
console.log(config); // undefined?
var config = loadConfig();
function loadConfig() {
return { env: "prod" };
}
This logs undefined
, not your config. Why? Because var config
got hoisted, but the assignment didn’t.
✅ Fix with const
:
const config = loadConfig();
console.log(config); // { env: "prod" }
Now there’s no room for confusion — either the config exists, or the program errors.
7. A Mental Model That Actually Works
Instead of memorizing rules, use this mental shortcut:
- var → Hoisted, initialized with
undefined
. - let/const → Hoisted, but “locked” in TDZ until declaration.
- function declaration → Fully hoisted (body + name).
- function expression/arrow/class → Hoisted like
let/const
(TDZ).
👉 Think of it as levels of readiness:
function foo() {}
→ Fully ready.var x
→ Exists, but empty.let/const
orclass
→ Exists but off-limits until initialized.
8. Why Hoisting Still Matters in 2025
You might ask: “Why care? My linter yells at me anyway.”
True, ESLint and TypeScript catch many hoisting issues. But:
- You’ll still read old codebases full of
var
. - Frameworks (React, Next.js, Node) can behave unexpectedly if you don’t understand initialization order.
- Debugging runtime errors often requires knowing why something is
undefined
vs throwing aReferenceError
.
👉 Understanding hoisting isn’t about memorization — it’s about mental clarity when debugging weird bugs.
9. Best Practices in Modern Code
- Default to const. Prevents unintentional hoisting bugs.
- Use let only when reassignment is intentional.
- Avoid var. Treat it as legacy (unless writing polyfills or working in old environments).
- Keep declarations at the top. Not required, but improves readability.
- Rely on ESLint rules like
no-use-before-define
.
"rules": {
"no-use-before-define": ["error", { "functions": false }]
}
10. A Quick Reference Table

Conclusion
Hoisting isn’t magic — it’s just how JavaScript preps your variables and functions before running code. The confusion comes from different rules for different declarations.
Here’s the TL;DR mental model:
- Functions declared with
function
are fully hoisted. - var is hoisted and set to undefined.
- let, const, and class are hoisted but locked in the TDZ.
👉 Pro Tip: If you stick to const
/let
and declare before using, you’ll almost never run into hoisting bugs in modern code.
Call to Action
What’s the weirdest bug you’ve hit because of hoisting?
💬 Drop your story in the comments.
🔖 Bookmark this guide for quick debugging reference.
👩💻 Share with your teammate who still thinks “hoisting = moving code up.”
Leave a Reply