- What Hoisting Actually Is
- The Two-Phase Mechanism
- Hoisting Rules Reference Table
- Temporal Dead Zone (TDZ)
- var Hoisting
- let and const Hoisting
- Function Declaration Hoisting
- Function Expression Hoisting
- Arrow Function Hoisting
- Class Hoisting
- Interview Problems (Beginner to Advanced)
- Hoisting with Closures
- Hoisting with Async Code
- Block Scope Edge Cases
- Advanced Collision Scenarios
- typeof Operator Behavior
- Common Production Bugs
- Mental Model for Interviews
- Interview Checklist
- Ultra-Hard Puzzles
Hoisting ≠ moving code to the top
This is the most common misconception. JavaScript doesn't physically move your code. Instead, it processes your code in two distinct phases:
During the creation phase, the JavaScript engine scans through your code and:
- Registers all variable and function declarations
- Allocates memory for them
- Initializes them according to specific rules (depends on declaration type)
During the execution phase, the code runs line by line with those pre-registered bindings already in place.
// What you write:
console.log(x);
var x = 5;
// How JS processes it:
// CREATION PHASE:
var x = undefined; // Memory allocated, initialized to undefined
// EXECUTION PHASE:
console.log(x); // undefined
x = 5; // Assignment happensCreation phase behavior by type:
| Declaration Type | Created | Initialized | Value |
|---|---|---|---|
var |
✅ | ✅ | undefined |
let |
✅ | ❌ | uninitialized (TDZ) |
const |
✅ | ❌ | uninitialized (TDZ) |
| Function declaration | ✅ | ✅ | Entire function |
| Function expression | Variable only | ❌ | undefined (if var) |
| Arrow function | Variable only | ❌ | undefined (if var) |
| Class | ✅ | ❌ | uninitialized (TDZ) |
Code runs line by line, assignments happen, functions are called.
| Construct | Hoisted | Initialized | Access before declaration | Scope |
|---|---|---|---|---|
var |
✅ | undefined |
✅ (returns undefined) |
Function |
let |
✅ | ❌ | ❌ (ReferenceError - TDZ) | Block |
const |
✅ | ❌ | ❌ (ReferenceError - TDZ) | Block |
| Function declaration | ✅ | ✅ | ✅ (fully usable) | Function/Block* |
| Function expression | Variable only | ❌ | ❌ | Depends on var/let/const |
| Arrow function | Variable only | ❌ | ❌ | Depends on var/let/const |
| Class | ✅ | ❌ | ❌ (ReferenceError - TDZ) | Block |
*Function declarations in blocks are complex and mode-dependent
The TDZ is the time between entering a scope and the actual declaration line where a variable exists but cannot be accessed.
console.log(a); // ❌ ReferenceError: Cannot access 'a' before initialization
let a = 10;The variable a exists in memory but is in the TDZ until line 2 executes.
{
// TDZ starts here for 'x'
console.log(x); // ❌ ReferenceError
let x = 1; // TDZ ends here
}function test() {
// TDZ starts for 'data'
console.log(data); // ❌ ReferenceError
let data = 100; // TDZ ends
}
test();function useX() {
console.log(x); // ❌ ReferenceError
}
let x = 10;
useX(); // This call itself is fine, but the function body accesses x before declarationWait, this actually works because by the time useX() is called, x is already declared. The TDZ only exists during the execution of the scope where the variable is declared.
Correct example:
let x = 10;
function useX() {
console.log(x); // ✅ 10 - works fine
}
useX();TDZ matters when accessing before declaration in the same scope:
function useX() {
console.log(x); // ❌ ReferenceError - TDZ
let x = 10; // Declaration in same scope
}
useX();console.log(typeof a); // ❌ ReferenceError
let a = 10;But with undeclared variables:
console.log(typeof undeclaredVariable); // ✅ "undefined"👉 Key insight: typeof does NOT bypass TDZ for declared variables.
var declarations are hoisted and initialized to undefined.
console.log(x); // undefined
var x = 5;
console.log(x); // 5Behind the scenes:
var x = undefined; // Creation phase
console.log(x); // undefined
x = 5; // Execution phase
console.log(x); // 5function test() {
console.log(a); // undefined
var a = 10;
console.log(a); // 10
}
test();function example() {
if (true) {
var x = 10;
}
console.log(x); // ✅ 10 - var ignores block scope
}
example();var a = 1;
var a = 2;
console.log(a); // 2 - allowed, no errorvar globalVar = 'test';
console.log(window.globalVar); // 'test' (in browsers)Both let and const are hoisted but not initialized, creating a TDZ.
console.log(x); // ❌ ReferenceError
let x = 10;console.log(y); // ❌ ReferenceError
const y = 20;{
let x = 10;
const y = 20;
}
console.log(x); // ❌ ReferenceError - not defined outside block
console.log(y); // ❌ ReferenceErrorlet a = 1;
let a = 2; // ❌ SyntaxError: Identifier 'a' has already been declaredconst b = 1;
const b = 2; // ❌ SyntaxErrorconst x; // ❌ SyntaxError: Missing initializer in const declarationFunction declarations are fully hoisted and initialized.
sayHello(); // ✅ "Hello!"
function sayHello() {
console.log("Hello!");
}Behind the scenes:
// Creation phase:
function sayHello() {
console.log("Hello!");
}
// Execution phase:
sayHello(); // ✅ "Hello!"foo(); // "second"
function foo() {
console.log("first");
}
function foo() {
console.log("second");
}Last declaration wins during creation phase.
Function expressions only hoist the variable, not the function.
sayHi(); // ❌ TypeError: sayHi is not a function
var sayHi = function() {
console.log("Hi!");
};Why TypeError, not ReferenceError?
// Behind the scenes:
var sayHi = undefined; // Hoisted
sayHi(); // undefined is not a function
sayHi = function() { // Assignment happens after
console.log("Hi!");
};greet(); // ❌ ReferenceError: Cannot access 'greet' before initialization
const greet = function() {
console.log("Greetings!");
};Arrow functions behave like function expressions.
sayHi(); // ❌ ReferenceError
const sayHi = () => {
console.log("Hi!");
};sayHi(); // ❌ TypeError: sayHi is not a function
var sayHi = () => {
console.log("Hi!");
};Classes are hoisted but remain in TDZ until declaration.
const obj = new Person(); // ❌ ReferenceError
class Person {
constructor(name) {
this.name = name;
}
}const obj = new MyClass(); // ❌ ReferenceError
const MyClass = class {
constructor() {}
};function test() {
console.log(a);
var a = 10;
}
test();Output: undefined
Explanation: var a is hoisted to the top of the function and initialized to undefined.
console.log(a);
var a = 1;
console.log(b);
let b = 2;Output:
undefined
ReferenceError: Cannot access 'b' before initialization
foo();
function foo() {
console.log("function");
}
var foo = function() {
console.log("var");
};Output: "function"
Explanation:
// Creation phase:
function foo() { console.log("function"); } // Function fully hoisted
var foo = undefined; // var is also hoisted but doesn't override function
// Execution phase:
foo(); // Calls the function
var foo = function() { console.log("var"); }; // Reassignment happens after"use strict";
{
function foo() {
return "inside block";
}
}
foo(); // ❌ ReferenceError in strict modeNon-strict mode (browser-dependent):
{
function foo() {
return "inside block";
}
}
foo(); // May work in some browsers👉 Best practice: Never use function declarations inside blocks. Use function expressions instead.
sayHi();
const sayHi = () => {
console.log("hi");
};Output: ReferenceError: Cannot access 'sayHi' before initialization
const obj = new Person();
class Person {
constructor(name) {
this.name = name;
}
}Output: ReferenceError: Cannot access 'Person' before initialization
var a = 1;
function test() {
console.log(a);
var a = 2;
}
test();Output: undefined
Explanation: Local var a shadows the outer a and is hoisted to the top of the function.
function foo(a) {
console.log(a);
var a = 20;
}
foo(10);Output: 10
Explanation: Parameter a = 10 is already initialized. The var a = 20 declaration is ignored (parameter takes precedence), but the assignment a = 20 would happen after the console.log.
var a = 1;
var a = 2;
console.log(a); // 2 - allowed
let b = 1;
let b = 2; // ❌ SyntaxError: Identifier 'b' has already been declaredfor (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}Output:
3
3
3
Explanation: var i is function-scoped (or global if not in a function), so there's only one binding shared across all iterations.
Fix with let:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Output: 0, 1, 2let creates a new binding for each iteration.
function createFunctions() {
var result = [];
for (var i = 0; i < 3; i++) {
result.push(function() {
return i;
});
}
return result;
}
const funcs = createFunctions();
console.log(funcs[0]()); // 3
console.log(funcs[1]()); // 3
console.log(funcs[2]()); // 3Explanation: All closures reference the same i variable.
Fix:
function createFunctions() {
var result = [];
for (let i = 0; i < 3; i++) {
result.push(function() {
return i;
});
}
return result;
}
const funcs = createFunctions();
console.log(funcs[0]()); // 0
console.log(funcs[1]()); // 1
console.log(funcs[2]()); // 2for (var i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}Output: 4, 4, 4 (after 1s, 2s, 3s)
Fix with IIFE:
for (var i = 1; i <= 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, j * 1000);
})(i);
}
// Output: 1, 2, 3Fix with let:
for (let i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}
// Output: 1, 2, 3async function test() {
console.log(x); // undefined
var x = 10;
console.log(x); // 10
}
test();Explanation: Hoisting works the same in async functions.
async function getData() {
console.log(data); // ❌ ReferenceError
const data = await fetch('/api');
}Explanation: const is in TDZ before declaration, even with await.
for (var i = 0; i < 3; i++) {
Promise.resolve().then(() => console.log(i));
}Output: 3, 3, 3
Fix:
for (let i = 0; i < 3; i++) {
Promise.resolve().then(() => console.log(i));
}
// Output: 0, 1, 2{
console.log(x); // ❌ ReferenceError
{
let x = 10;
}
}if (true) {
var x = 10;
}
console.log(x); // ✅ 10if (true) {
let y = 20;
}
console.log(y); // ❌ ReferenceErrorswitch (1) {
case 1:
let x = 10;
console.log(x); // 10
break;
case 2:
let x = 20; // ❌ SyntaxError: Identifier 'x' has already been declared
break;
}Explanation: The entire switch block is one scope.
Fix:
switch (1) {
case 1: {
let x = 10;
console.log(x);
break;
}
case 2: {
let x = 20;
console.log(x);
break;
}
}for (let i = 0; i < 3; i++) {
let i = 'inner'; // Different variable
console.log(i); // 'inner', 'inner', 'inner'
}console.log(foo); // [Function: foo]
var foo = "variable";
function foo() {
return "function";
}
console.log(foo); // "variable"Explanation:
// Creation phase:
function foo() { return "function"; } // Function hoisted
var foo; // var declaration (but doesn't override the function)
// Execution phase:
console.log(foo); // [Function: foo]
foo = "variable"; // Assignment happens
console.log(foo); // "variable"foo(); // "third"
function foo() {
console.log("first");
}
function foo() {
console.log("second");
}
function foo() {
console.log("third");
}Explanation: Last function declaration wins.
function test(x) {
console.log(x); // 10
var x;
console.log(x); // 10
x = 20;
console.log(x); // 20
}
test(10);Explanation: Parameter declaration takes precedence. var x; is redundant.
function test(a = b, b = 2) {
console.log(a, b);
}
test(); // ❌ ReferenceError: Cannot access 'b' before initializationExplanation: Parameters are evaluated left to right. b is in TDZ when a's default is evaluated.
Fix:
function test(b = 2, a = b) {
console.log(a, b);
}
test(); // 2, 2console.log(typeof undeclaredVariable); // "undefined"console.log(typeof x); // ❌ ReferenceError
let x = 10;console.log(typeof y); // "undefined"
var y = 10;❌ Buggy:
function checkStatus() {
if (!isReady) {
var isReady = true;
}
return isReady;
}
console.log(checkStatus()); // undefined (not true!)Behind the scenes:
function checkStatus() {
var isReady; // Hoisted to top, initialized as undefined
if (!isReady) { // undefined is falsy
isReady = true;
}
return isReady;
}✅ Fix:
function checkStatus() {
let isReady = false;
if (!isReady) {
isReady = true;
}
return isReady;
}
console.log(checkStatus()); // true❌ Buggy:
for (var i = 0; i < buttons.length; i++) {
buttons[i].onclick = function() {
alert(i); // Always alerts buttons.length
};
}✅ Fix:
for (let i = 0; i < buttons.length; i++) {
buttons[i].onclick = function() {
alert(i); // Alerts correct index
};
}❌ Buggy:
for (var i = 0; i < 5; i++) {
fetch('/api/' + i).then(response => {
console.log('Response for', i); // Always 5
});
}✅ Fix:
for (let i = 0; i < 5; i++) {
fetch('/api/' + i).then(response => {
console.log('Response for', i); // Correct index
});
}When explaining hoisting in interviews, use this mental model:
"JavaScript processes code in two phases. During the creation phase, it scans through the scope and registers all variable and function declarations, allocating memory and initializing them according to specific rules—
vargetsundefined, functions get their full definition, andlet/const/classes enter a temporal dead zone where they exist but can't be accessed. Then during the execution phase, the code runs line by line with those pre-registered bindings already in place. This is why you can call a function before it's declared, but accessing aletvariable before declaration throws a ReferenceError."
Key phrases to use:
- "Two-phase mechanism: creation and execution"
- "Memory allocation vs initialization"
- "Temporal Dead Zone for let/const"
- "Scope registration before execution"
- "Function declarations are fully initialized"
✅ Function declarations → Fully hoisted and initialized
✅ var → Hoisted, initialized to undefined
✅ let/const/class → Hoisted but in TDZ, uninitialized
✅ Arrow functions → Not hoisted as functions
✅ Function expressions → Variable hoisted (if var), function not
✅ Parameters → Take precedence over var in same scope
✅ Closures + var → Single shared binding (common bug)
✅ Block scope → let/const respect blocks, var doesn't
✅ typeof → Doesn't bypass TDZ
✅ Switch cases → Share one block scope
var a = 1;
function outer() {
console.log(a);
function inner() {
console.log(a);
var a = 3;
}
inner();
console.log(a);
var a = 2;
}
outer();
console.log(a);Output:
undefined
undefined
undefined
1
Explanation:
- First
console.log(a)inouter: localvar ais hoisted →undefined - In
inner,var ais hoisted →undefined - After
inner(), stillundefinedinouter - After
outer(), globalais still1
var foo = 1;
function bar() {
console.log(foo);
foo = 10;
function foo() {}
console.log(foo);
}
bar();
console.log(foo);Output:
[Function: foo]
10
1
Explanation:
- Function
foois hoisted inbar's scope foo = 10reassigns the local function- Global
fooremains1
let x = 1;
function test() {
console.log(x);
let x = 2;
}
test();Output: ReferenceError: Cannot access 'x' before initialization
Explanation: Local let x creates TDZ in test, shadowing outer x.
var x = 1;
{
var x = 2;
{
let x = 3;
console.log(x);
}
console.log(x);
}
console.log(x);Output:
3
2
2
Explanation: var ignores block scope, let respects it.
for (var i = 0; i < 3; i++) {
setTimeout(() => {
var j = i;
console.log(j);
}, 100);
}Output: 3, 3, 3 (after 100ms)
Explanation: var i is shared. Each timeout captures the final value of i.
const MyClass = class InternalName {
constructor() {
console.log(InternalName);
}
};
new MyClass(); // [class InternalName]
console.log(InternalName); // ❌ ReferenceErrorExplanation: Class expression name is only visible inside the class.
console.log(a); // ❌ ReferenceError
let { a } = { a: 1 };console.log(b); // undefined
var { b } = { b: 2 };Explanation: Destructuring follows the same hoisting rules as regular declarations.
var x = 1;
function foo(x = x) {
console.log(x);
}
foo(); // ❌ ReferenceError: Cannot access 'x' before initializationExplanation: Parameter x is in TDZ when its own default value is evaluated.
function outer() {
inner(); // ✅ Works
function inner() {
console.log("inner");
}
}
outer();function outer() {
inner(); // ❌ ReferenceError
const inner = function() {
console.log("inner");
};
}
outer();console.log(typeof foo); // "function"
console.log(typeof bar); // "undefined"
var bar = function() {};
function foo() {}Explanation: Function declaration hoisted completely, function expression only hoists the variable.
var obj = { a: 1 };
with (obj) {
console.log(a); // 1
var a = 2; // Creates global variable!
}
console.log(a); // 2
console.log(obj.a); // 1Explanation: var inside with doesn't create a property on obj, it creates a global variable.
function test() {
eval("var x = 10");
console.log(x); // 10 (in non-strict mode)
}
test();In strict mode:
"use strict";
function test() {
eval("var x = 10");
console.log(x); // ❌ ReferenceError
}
test();gen(); // ❌ ReferenceError
const gen = function* () {
yield 1;
};gen(); // ✅ Works
function* gen() {
yield 1;
}Explanation: Generator expressions follow function expression rules, generator declarations follow function declaration rules.
test(); // ❌ TypeError: test is not a function
var test = () => console.log("arrow");// This works:
myFunction();
import { myFunction } from './module.js';Explanation: Imports are hoisted to the top of the module and initialized before any code runs.