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/constorclass→ 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
undefinedvs 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
functionare 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