JavaScript Hoisting Explained: var, let, const, and Common Mistakes


JavaScript hoisting is one of the most misunderstood behaviors in JavaScript. It allows variables and functions to be used before they are declared, but this can lead to confusing bugs if not fully understood.

Have you ever wondered why JavaScript lets you use a variable before you declare it? Or why some functions seem to “magically” work even when they’re called first?

This behavior is due to JavaScript hoisting, a core concept that often confuses beginners.

Welcome to the fascinating world of hoisting in JavaScript. It’s one of the most common sources of confusion for beginners—and even experienced developers—because it’s not immediately visible in the code.

In this deep-dive post, we’ll explore:

  1. What hoisting really means

  2. How it behaves with var, let, and const

  3. Function declarations vs expressions

  4. Temporal Dead Zone (TDZ)

  5. Internal mechanics of the JavaScript engine

  6. Real-world bugs caused by hoisting

  7. Best practices to write clean, predictable code

Let’s get started. 🎯


🔍 What is Hoisting?

JavaScript hoisting is the behavior where variable and function declarations are moved to the top of their scope during compilation.

Hoisting is a behavior in JavaScript where declarations are moved to the top of their scope during the compilation phase, before the code is executed.

But there’s a catch:

Only declarations are hoisted — not initializations.

This means JavaScript sets aside space in memory for your variables and functions before actually running the code. That’s why you can reference some variables or functions before they appear in your code — though not always safely.


📦 JavaScript Hoisting with var, let, and const

Let’s break it down by keyword.

1. var – The Classic Culprit

javascript code:
console.log(name); // undefined
var name = "Alice";

This works because:

  • The var name; declaration is hoisted

  • The assignment name = "Alice" stays in place

  • So at the time of console.log, name exists but is undefined

Because of JavaScript hoisting, var is lifted to the top and initialized as undefined, allowing early access before the declaration line.

Behind the scenes, the engine sees:

javascript code:
var name;
console.log(name); // undefined
name = "Alice";

2. let and const – The Safer Modern Way

javascript code:
console.log(age); // ❌ ReferenceError
let age = 25;

console.log(city); // ❌ ReferenceError
const city = "Hanoi";

These throw ReferenceErrors, even though they’re hoisted too!

So what’s going on?


🕳️ Temporal Dead Zone and JavaScript Hoisting

The Temporal Dead Zone is the period between entering the block scope and the point where the variable is declared.

Understanding how let and const behave during JavaScript hoisting helps you avoid the Temporal Dead Zone (TDZ) and related errors.

During this time:

  • The variable exists

  • But you can’t access it — doing so will throw an error

javascript code:
{
// TDZ for `score` starts here
console.log(score); // ❌ ReferenceError
let score = 100;
// TDZ ends after this line
}

This helps prevent bugs and makes code behavior more predictable — a major improvement over var.


⚙️ Function Hoisting – Declarations vs Expressions

1. Function Declarations – Fully Hoisted

javascript code:
sayHello(); // ✅ "Hello!"

function sayHello() {
console.log("Hello!");
}

Function declarations are fully hoisted — both name and body — so you can use them before they’re defined.

2. Function Expressions – Partially Hoisted

javascript code:
sayHi(); // ❌ TypeError: sayHi is not a function

var sayHi = function () {
console.log("Hi!");
};

Why TypeError? Because sayHi is hoisted as a variable (with undefined), not a function. So you’re calling undefined().

3. Arrow Functions – Same as Expressions

javascript code:
greet(); // ❌ TypeError

const greet = () => {
console.log("Hi!");
};

Just like function expressions, arrow functions are not hoisted as functions.


🧬 Behind the Scenes – What the Engine Actually Does

When JavaScript code is run, two phases happen:

1. Compilation Phase (before execution)

  • The engine scans the code

  • Allocates memory for all declarations

    • var variables: initialized to undefined

    • let/const: hoisted but uninitialized (TDZ begins)

    • Function declarations: hoisted with body

2. Execution Phase

  • Code runs line by line

  • Assignments happen

  • Any usage before declaration for let/const throws an error


💥 Common Bugs Caused by JavaScript Hoisting

Here’s a real-world example of how misunderstanding JavaScript hoisting can introduce subtle and hard-to-debug issues.

Bug:

javascript code:
function isAdult(age) {
if (age >= 18) {
var message = "You are an adult.";
}
return message; // ❗ Always defined, even if age < 18
}

You might expect message to be undefined when age < 18, but due to var hoisting, the variable is declared at the top of the function.

So internally, it becomes:

javascript code:
function isAdult(age) {
var message;
if (age >= 18) {
message = "You are an adult.";
}
return message;
}

Fix:

Use let to limit scope:

javascript code:
function isAdult(age) {
if (age >= 18) {
let message = "You are an adult.";
return message;
}
return "You are not an adult.";
}

✅ Summary Table: Hoisting Behavior

Keyword/TypeHoisted?Initialized?Accessible Before Declaration?TDZ Applies?
varYesYes (undefined)✅ Yes (but undefined)❌ No
letYesNo❌ ReferenceError✅ Yes
constYesNo❌ ReferenceError✅ Yes
Function DeclarationYesYes (full body)✅ Yes❌ No
Function ExpressionYes (var)No❌ TypeError / ReferenceError✅ Yes
Arrow FunctionYes (const/let)No❌ TypeError / ReferenceError✅ Yes

🛡️ Best Practices to Avoid Hoisting Bugs

  1. Always declare variables at the top of their scope

  2. Use let or const instead of var

  3. Don’t use variables before declaring them—even if they’re hoisted

  4. Prefer function declarations only when you must call a function early

  5. Turn on 'use strict'; to catch silent hoisting errors


🧘 Final Thoughts

Understanding hoisting helps you write better, safer JavaScript. It’s not just a quirky language feature — it’s how the engine works under the hood. And once you internalize it, you’ll stop being surprised by weird bugs.

Mastering JavaScript hoisting is essential for writing predictable and bug-free code, especially when working with variables and functions across different scopes.

Think of hoisting like packing your backpack before school — JS packs all your variables and functions first, then starts the school day (execution)!


✨ Bonus Tip: Use ESLint

Tools like ESLint can catch hoisting-related issues early. For example, rules like no-use-before-define can warn you when you’re using variables too early.