1215 words
6 minutes
Deep Dive into JavaScript Hoisting: Beyond the Rote Answers

Many developers with three to five years of JavaScript experience give the exact same canned response when asked “What is hoisting?” during an interview: “Hoisting means variable declarations are moved to the top of their scope.”

That sounds fine on the surface, but probe a bit deeper:

  • What is the difference between var, let, and const regarding hoisting?
  • Which gets hoisted higher: function declarations or function expressions?
  • What exactly is the Temporal Dead Zone (TDZ)?
  • Why is a evaluated as undefined instead of 10 when var a = 10 is hoisted?

At this point, answers often become vague and uncertain. Instead of memorizing interview cheat sheets, let’s start where JS execution actually begins—the compilation phase—to break down hoisting once and for all.

1. The Essence of Hoisting: “Registration,” Not “Relocation”#

Many people assume hoisting means the browser physically shifts code to the top of the file. That is completely inaccurate.

The reality: Before executing code, the JS engine performs a “compilation scan” to complete the binding and registration of variables and functions. This process is called the Declaration Phase, whereas the actual execution is the Assignment Phase.

console.log(a);
var a = 10;

What actually happens?

  1. Compilation phase: The JS engine scans the code, encounters var a, and registers variable a in the environment record of the current scope, initializing it to undefined.
  2. Execution phase: The engine reaches console.log(a) and reads a, which is currently undefined. Continuing down to a = 10, it finally assigns 10 to a.

Thus, what appears to be “hoisting” is fundamentally: declarations are completed at compile time; assignments are completed at runtime.

2. var: The Most Misunderstood Hoisting Behavior#

The True Face of var#

console.log(a); // undefined
var a = 10;

This is equivalent to:

var a; // Compilation phase: declaration + initialized to undefined
console.log(a); // Execution phase: prints undefined
a = 10; // Execution phase: assignment

Three critical points to remember:

  • ✅ Declaration is hoisted
  • ✅ Automatically initialized to undefined
  • ❌ Assignment is not hoisted

This explains why you can access a variable that has been declared but not yet assigned without throwing a runtime error.

Function Scope of var#

function test() {
console.log(a);
if (false) {
var a = 10;
}
}
test(); // undefined

Even though if (false) never executes, var a is still hoisted and belongs to the entire function scope.

3. Function Hoisting: Taking Precedence over Variables#

Function Declarations: Complete Hoisting#

foo();
function foo() {
console.log('hello');
}

✅ Executes normally. During the compilation phase, function declarations complete both declaration and assignment (pointing directly to the function body). This is equivalent to:

function foo() {
console.log('hello');
}
foo();

Function Expressions: Following var Rules#

foo(); // TypeError: foo is not a function
var foo = function () {
console.log('hello');
};

Here, foo follows the exact same hoisting rules as var:

var foo; // Compilation phase: foo = undefined
foo(); // Execution phase: undefined(), crashes immediately
foo = function () { ... }; // Assigned later

⚠️ Pay attention to error types:

  • ReferenceError: The variable has not been declared at all.
  • TypeError: The variable exists, but its type is incorrect (e.g., trying to invoke undefined as a function).

Precedence: Function Declarations > Variable Declarations#

console.log(foo);
var foo = 'bar';
function foo() {}

Output: ƒ foo() {}

Reason: During the compilation phase, both function declarations and variable declarations are hoisted, but function declarations take higher priority and overwrite the placeholder slot for variables of the same name. However, if the variable is assigned a value during the execution phase, it will overwrite the reference back:

console.log(foo); // ƒ foo() {}
var foo = 'bar';
function foo() {}
console.log(foo); // 'bar'

4. let / const: Are They Really “Not Hoisted”?#

This is where the single biggest misconception lies.

Enter the TDZ (Temporal Dead Zone)#

console.log(a); // ReferenceError
let a = 10;

From this, many jump to the conclusion that let is not hoisted. ❌ That conclusion is inaccurate.

In reality: let and const are hoisted, but they are not automatically initialized.

The ES6 specification introduced the Temporal Dead Zone (TDZ): spanning from the start of the scope up to the let / const declaration statement. During this window, although the variable already exists in the scope, it cannot be accessed, read, or written to.

{
// TDZ starts
console.log(a); // ❌ Throws ReferenceError
let a = 10; // TDZ ends
}

Core Differences: let vs var#

Featurevarlet / const
Is hoisted?
Auto-initialized?✅ (undefined)
ScopeFunction-scopedBlock-scoped
TDZ applies?

Summary in one line: var is hoisted and initialized; let / const are hoisted but uninitialized.

The Special Constraint of const#

const a; // SyntaxError: Missing initializer in const declaration

const not only lacks automatic initialization, but it must be assigned a value at declaration, otherwise a syntax error is thrown immediately.

5. Dissecting Classic Interview Questions Step-by-Step#

Question 1: Scope Shadowing Traps#

var a = 1;
function foo() {
console.log(a);
var a = 2;
}
foo();

Answer: undefined

Explanation: Inside foo, var a is declared. Within the function scope, a is hoisted and initialized to undefined, shadowing the outer global variable a. Therefore, undefined is logged.

Question 2: Mixed Hoisting & TDZ#

console.log(typeof a);
var a = 1;
let b = 2;

Answer:

'undefined' // typeof a
ReferenceError // referencing b

Explanation: var a is hoisted and initialized to undefined, so typeof a safely returns 'undefined'. let b resides inside the TDZ, so referencing b (even using typeof b) immediately throws a ReferenceError. Recognizing typeof behavior inside the TDZ is a big bonus in technical interviews.

Question 3: var vs let in Loops#

for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Outputs: 3, 3, 3
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Outputs: 0, 1, 2

Reason: var i — the entire loop shares a single function-scoped binding of i. let i — a new block-scoped binding for i is created for each iteration step. This is a practical demonstration of block scoping combined with hoisting rules.

6. Why Does Hoisting Exist? (Design Motivations)#

Now that we understand what it is, let’s step back and look at why.

Supporting Mutual Function Invocation#

function a() {
b();
}
function b() {
a();
}

Without hoisting, whichever function were placed first wouldn’t be able to find the other. Hoisting ensures all functions are registered during compilation, allowing mutual calls regardless of declaration order.

Historical Context#

JavaScript was originally designed as a lightweight browser scripting language. To lower the barrier to entry, early language designers opted for automatic variable initialization and fault tolerance over strict error throwing. While reasonable at the time, these decisions left behind the quirks we navigate today.

7. Best Practices for Modern JS#

Now that you understand hoisting, how should you structure your code in production?

  • Default to let / const: Avoid var entirely, enforce explicit block scopes, and prevent implicit bugs outside the TDZ.

  • Declare before use: Even if let technically hoists declarations behind the scenes, don’t rely on it:

    // Bad
    console.log(a);
    let a = 10;
    // Good
    let a = 10;
    console.log(a);
  • Prefer function expressions / arrow functions:

    // Recommended
    const greet = () => {
    console.log('hi');
    };

    Compared to function declarations, arrow functions offer predictable hoisting, align with const immutability semantics, and integrate seamlessly with modules and tree-shaking.

8. Summary in One Sentence#

Hoisting is not “code relocation”; it is “compile-time registration.”

  • var: Hoisted + auto-initialized → access allowed early, value is undefined.
  • let / const: Hoisted, but uninitialized → access within the TDZ throws a ReferenceError.
  • Function declarations: Fully hoisted, taking precedence over variable placeholders.
  • Function expressions: Follow standard variable hoisting rules (var or let / const).

Once you can explain hoisting through the lens of Compilation → Execution → Scope → TDZ, you move past simple interview memorization to a true understanding of JavaScript execution mechanics.

Deep Dive into JavaScript Hoisting: Beyond the Rote Answers
https://astro-nyc.pages.dev/posts/javascript-hoisting-deep-dive/
Author
Hari Seldon
Published at
2026-08-06
License
CC BY-NC-SA 4.0