Why You Get ReferenceError: A Practical Guide to JavaScript Hoisting

MMuhammad Naeem
November 13, 2025
6 min read
86 views

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

javascript visualize hoisting

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

limits-of-temporal-dead-zone-javascript

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

  1. Always use let and const over var.
    They give you early error feedback and safer scoping.

  2. Declare variables before using them.
    Even though hoisting makes them available later, don’t rely on it.

  3. Use "use strict";
    It disallows undeclared variables and promotes clean execution.

  4. Define functions before use.
    Avoid mixing declarations and expressions for clarity.

  5. 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).

Comments (0)

Join the conversation

Sign in to share your thoughts and engage with other readers.

No comments yet

Be the first to share your thoughts!