Why You Get ReferenceError: A Practical Guide to JavaScript Hoisting

Introduction
Have you ever wondered how JavaScript can call a function before it’s even defined, or why using let or const before declaration throws that infamous ReferenceError?
Welcome to one of the most misunderstood yet absolutely fundamental topics in JavaScript, Hoisting.
By the end of this guide, you’ll not only understand what hoisting really is, but also why it exists, how it works under the hood, and how to write bug-free code by mastering it.
Why Most Tutorials Fail You
Most tutorials say something like “JavaScript moves declarations to the top,” which is technically true but dangerously incomplete.
They skip over the real mechanics, the compilation phase, the difference between declaration and initialization, and the Temporal Dead Zone (TDZ), leading to half-understood behavior and confusing errors.
We’ll fix that here.
1. The Mental Model
Think of JavaScript execution like a stage play with two acts:
Act 1: Setup (Compilation Phase) → The engine builds the stage, lists all the characters (variables and functions), and assigns them their dressing rooms (memory spaces).
Act 2: Performance (Execution Phase) → The code actually runs. The characters come on stage and start interacting.
Hoisting happens entirely in Act 1, it’s how JavaScript prepares the scene before running your code.
Recommended Read:
If you’ve read my detailed guide on Execution Context in JavaScript, you already have a clear picture of these two phases.
2. What is Hoisting in JavaScript?
Hoisting means that all declarations are processed before code execution begins, so variables and functions are “known” to the engine throughout their scope.
Key Takeaway:
Hoisting doesn’t mean “moving code.” It means memory for declarations is reserved before execution, but initialization timing determines whether you can safely access them.
JavaScript's hoisting is a unique behavior that allows variables and functions to be accessed before they are declared in the code.
JavaScript Hoisting Explained Visually

3. Step-by-Step: How Hoisting Actually Works
Let’s unpack the process as the JS engine sees it.
Step 1: The Compilation (Memory Creation Phase)
When JavaScript parses your code, it:
Scans for declarations (var, let, const, function, class).
Stores them in memory (Variable Environment and Lexical Environment).
Sets initial values:
var → undefined
let / const → uninitialized (TDZ)
function → entire function body
Example:
console.log(a); // undefined
console.log(b); // ReferenceError
console.log(sum()); // Works fine
var a = 10;
let b = 20;
function sum() {
return 2 + 3;
}
The engine “knows” about all three, but only a and sum are initialized before execution starts.
Step 2: The Execution (Code Runs Line by Line)
Now the engine runs your code top-to-bottom:
When it hits var a = 10, it reassigns a from undefined to 10.
When it hits let b = 20, it initializes b for the first time, exiting the Temporal Dead Zone (TDZ).
Function calls use the already-hoisted sum() binding.
Step 3: The Temporal Dead Zone (TDZ) in Action
The TDZ is the “danger zone” between a variable’s creation (hoisting) and its initialization.
Example:
{
console.log(x); // ReferenceError
let x = 5; // initialized here
console.log(x); // 5
}
During TDZ, the variable exists in memory but is locked. It can’t be touched until the declaration line runs.
Analogy:
Imagine your variable is a concert ticket, it’s printed and stored at the gate (hoisted), but you can’t use it until the gates open (declaration executes).
JavaScript Temporal Dead Zone (TDZ) Explained Visually

4. Function Hoisting Explained
Functions play by their own rules.
Function Declarations, Fully Hoisted
You can call them before they’re defined:
greet(); // Works fine
function greet() {
console.log("Hello!");
}
Both the name and the body are hoisted.
Function Expressions, Partially Hoisted
Only the variable name is hoisted, not the assigned function.
sayHi(); // TypeError: sayHi is not a function
var sayHi = function() {
console.log("Hi!");
};
Here, sayHi is hoisted as undefined. Calling it early is like trying to execute undefined.
Arrow Functions with let or const
Same as function expressions, but worse if called early, because they’re in the TDZ:
greet(); // ReferenceError
const greet = () => console.log("hello");
Key Takeaway:
Declarations → fully hoisted (safe)
Expressions (var) → hoisted as undefined (unsafe)
Expressions (let/const) → in TDZ (very unsafe)
5. The 99% Problem Solver: Common Gotchas and Pitfalls
Let’s address the three biggest hoisting traps that trip up developers.
Pitfall 1: Misinterpreting Hoisting as Code Movement
The Mistake: Thinking JavaScript moves declarations up.
The Fix: Remember, JavaScript doesn’t move your code, it prepares memory during the compilation phase.
Code isn’t rearranged; the engine just registers declarations before running.
Pitfall 2: The var “Undefined” Trap
Example:
if (false) {
var msg = "Hi!";
}
console.log(msg); // undefined
Even though the if never runs, the declaration is hoisted globally, msg exists!
The Fix: Use let or const (block-scoped) to prevent ghost variables leaking outside blocks.
Pitfall 3: TDZ Confusion in Nested Scopes
Example:
let a = 10;
function check() {
console.log(a); // ReferenceError (new a in TDZ)
let a = 20;
}
check();
The inner let a shadows the outer one, creating a new TDZ.
The console.log refers to the inner a, which isn’t initialized yet.
The Fix: Always declare variables at the top of their block to minimize TDZ confusion.
6. Writing Hoisting-Safe Code: Best Practices
Always use let and const over var.
They give you early error feedback and safer scoping.Declare variables before using them.
Even though hoisting makes them available later, don’t rely on it.Use "use strict";
It disallows undeclared variables and promotes clean execution.Define functions before use.
Avoid mixing declarations and expressions for clarity.Understand TDZ to debug like a pro.
When you see a ReferenceError, ask yourself: “Is this variable still in its TDZ?”
7. Conclusion & Next Steps
You’ve just mastered one of JavaScript’s most misunderstood yet most powerful concepts, Hoisting.
Now you understand what actually happens during compilation, why TDZ exists, and how to write safer, clearer code.
Key Takeaways:
Hoisting = declaration memory setup before execution.
var → initialized as undefined.
let / const → hoisted but uninitialized (TDZ).
Functions → fully hoisted.
Classes → hoisted but not initialized (TDZ).
Join the conversation
Sign in to share your thoughts and engage with other readers.
No comments yet
Be the first to share your thoughts!