Core Concepts9 min read · Updated 2025-06-01

JavaScript Hoisting Explained: What Actually Moves, What Doesn't, and Why It Matters

Hoisting isn't the engine moving your code around — it's the engine processing declarations before execution. Understanding the difference explains every hoisting surprise you've ever encountered.

💡 Practice these concepts interactively with AI feedback

Start Practicing →

Hoisting has a reputation for being confusing, and most of that confusion comes from one bad metaphor: "JavaScript moves your declarations to the top of the file." Nothing moves. Your source code stays exactly as you wrote it.

What actually happens is subtler — and once you understand it, every hoisting quirk becomes completely predictable.

What Hoisting Actually Is

Before JavaScript executes any code in a scope, it does a pass through that scope and registers all declarations. It finds every var, every function, every let, every const, and sets them up in the environment. Then — and only then — execution begins from the top.

The effect looks like declarations moved to the top because they're available before their source line runs. But the code never moved. The engine just knew about them before running the first line.

var: Declared and Initialized Early

var declarations get the most generous treatment. They're registered and initialized to undefined before execution starts:

console.log(name)  // undefined — not an error
var name = 'Alice'
console.log(name)  // 'Alice'

Mentally, the engine sees this as:

var name = undefined  // registered + initialized (hoisting effect)

console.log(name) // undefined name = 'Alice' // the assignment stays here console.log(name) // 'Alice'

The declaration hoisted. The assignment did not. This distinction — declaration vs assignment — is the core of understanding hoisting.

var is function-scoped, not block-scoped:

function example() {
  if (true) {
    var x = 10  // var ignores the block — scoped to the function
  }
  console.log(x)  // 10 — x is visible throughout the function
}

This is why var inside loops, ifs, and any block leaks into the containing function — because var doesn't see blocks as scope boundaries.

Function Declarations: Fully Hoisted

Function declarations — the function name() {} syntax — are hoisted completely. Name and body, before anything runs:

greet('Alice')  // 'Hello, Alice' — works before the declaration

function greet(name) { return Hello, ${name} }

This is intentional and useful. You can organize code by putting utility functions at the bottom and calls at the top. The engine has already registered greet before line 1 executes.

let and const: The Temporal Dead Zone

let and const are also hoisted — but not initialized. From the start of the block to the declaration line, the variable exists in a state where it cannot be read or written. This region is the Temporal Dead Zone (TDZ).

console.log(age)  // ReferenceError: Cannot access 'age' before initialization
let age = 25

The error message is precise: "cannot access before initialization." The variable exists — it was hoisted — but it hasn't been initialized yet. That's the TDZ.

{
  // TDZ starts here for 'color'
  console.log(color)  // ReferenceError
  // TDZ ends here ↓
  let color = 'blue'
  console.log(color)  // 'blue'
}

Why does the TDZ exist? var's behavior — being undefined before its assignment — is genuinely confusing. You read a variable and get undefined, with no indication whether it was intentionally undefined or just unset. The TDZ makes the mistake loud and immediate: a hard error instead of a silent undefined.

Function Expressions and Arrow Functions: Not Hoisted

Only the variable is hoisted — not the function. And since const/let variables start in the TDZ, accessing them before assignment throws:

// With var — partially hoisted, but wrong result
console.log(add(1, 2))   // TypeError: add is not a function
var add = function(a, b) { return a + b }

// With const — TDZ error console.log(multiply(2, 3)) // ReferenceError: Cannot access 'multiply' before initialization const multiply = (a, b) => a * b

This is a critical distinction. A function declaration can be called before its definition. A function expression cannot.

Class Declarations: Hoisted but TDZ-Protected

Classes hoist like let and const — they're registered but not initialized, so accessing them before the declaration throws:

const obj = new MyClass()  // ReferenceError: Cannot access 'MyClass' before initialization

class MyClass { constructor() { this.x = 1 } }

This is deliberate. A class defined before its base class would create impossible situations. The TDZ prevents it.

Hoisting in the Same Scope: When Names Collide

What happens when a var and a function declaration share the same name?

console.log(typeof foo)  // 'function'

var foo = 'string' function foo() { return 42 }

console.log(typeof foo) // 'string'

Function declarations win over var during hoisting — when both are processed, the function takes priority. Then execution runs: foo is reassigned to 'string'. By the end, foo is a string. This is a sign that something has gone wrong in the code — names should never collide like this.

The Output Question You'll See in Interviews

function outer() {
  console.log(x)  // what logs here?
  var x = 1
  console.log(x)  // what logs here?

function inner() { console.log(x) // what logs here? var x = 2 console.log(x) // what logs here? }

inner() console.log(x) // what logs here? } outer()

Output: undefined, 1, undefined, 2, 1

Trace: outer hoists var x (undefined) and function inner. First log: undefined. Assignment: x = 1. Second log: 1. Calling inner: inner has its own var x hoisted to undefined. Third log: undefined. Assignment x = 2. Fourth log: 2. Back in outer, x is still 1inner had its own x. Fifth log: 1.

The key: every function scope has its own hoisting. inner's var x does not affect outer's var x.

A Practical Rule Set

Use const by default. No hoisting surprise, no accidental reassignment, intention is clear.

Use let when reassignment is needed. TDZ protects you from use-before-declaration bugs.

Use function declarations for named utilities. Intentional hoisting lets you put the important code at the top and helpers at the bottom.

Never use var in new code. Its function-scoped, initialization-to-undefined behavior has caused more bugs than it has ever prevented. Every var can be replaced with const or let.

Never call a function expression before its line. Unlike declarations, expressions aren't callable until execution reaches their assignment.

The Mental Model That Makes It Click

Think of JavaScript execution as two-phase:

Phase 1 — Setup (hoisting): The engine scans the scope. Every var is registered and set to undefined. Every function declaration is registered with its full body. Every let, const, and class is registered but left uninitialized (TDZ).

Phase 2 — Execution: Code runs line by line. Assignments happen. Function expressions are created. let and const become initialized when their declaration line is reached.

With this model, every hoisting behavior follows logically. var before its assignment: undefined (initialized in phase 1, assigned in phase 2). Function declaration before its definition: works (fully set up in phase 1). let before its declaration: ReferenceError (registered but not initialized — TDZ).

Nothing moves. The engine just knows some things before you tell it.

📚 Practice These Topics
Var let const
3–5 questions
Hoisting
4–8 questions
Scope
4–6 questions
Execution context
3–6 questions

Put This Into Practice

Reading articles is passive. JSPrep Pro makes you actively recall, predict output, and get AI feedback.

Start Free →Browse All Questions

Related Articles

Core Concepts
map() vs forEach() in JavaScript: Which One to Use and Why It Matters
7 min read
Core Concepts
Arrow Functions vs Regular Functions in JavaScript: 6 Key Differences
9 min read
Interview Prep
Promise.all vs allSettled vs race vs any: The Complete Comparison
9 min read