JavaScript Scope and Hoisting Explained (With Examples & Interview Questions)
Scope & Hoisting in
JavaScript Explained
Master JavaScript scope and hoisting completely — global, local, block scope, var vs let vs const differences, hoisting rules, temporal dead zone, execution context, common mistakes & interview questions.
What is Scope in JavaScript? — The Complete Explanation
Scope in JavaScript determines where a variable can be accessed in your code. It defines which parts of your program can read or modify a variable — and which parts cannot. Scope is one of the most fundamental concepts in JavaScript, and misunderstanding it causes some of the most confusing bugs that developers encounter at every level.
Think of scope like the rooms in a house. A variable declared inside a specific room (inside a function or block) can only be used inside that room. A variable declared in the hallway (global scope) is accessible from every room. The rules that determine what is accessible from where — that is scope.
JavaScript has three types of scope: Global Scope (accessible everywhere), Local/Function Scope (accessible only inside a function), and Block Scope (accessible only inside a { } block). Understanding how var, let, and const behave differently with scope is absolutely essential for writing correct, professional JavaScript code in 2026.
Global Scope — Variables Accessible Everywhere
A variable has global scope when it is declared outside all functions and blocks — at the top level of your JavaScript file. Global variables can be read and modified from anywhere in your entire program — inside functions, inside loops, inside conditions — absolutely everywhere.
While global variables are convenient, using too many of them is considered bad practice in professional JavaScript. Any function in your program can accidentally overwrite a global variable, causing hard-to-find bugs. This is why experienced developers minimize global variables and prefer function or block scope instead.
// Global scope — declared outside everything let appName = "MyApp"; const MAX_USERS = 100; var userCount = 0; function addUser(name) { userCount++; // ✅ Can access global userCount console.log(`${appName}: "${name}" added. Total: ${userCount}`); } function checkCapacity() { if (userCount >= MAX_USERS) { console.log("❌ App is full!"); } else { console.log(`✅ ${MAX_USERS - userCount} spots remaining`); } } addUser("Sara"); addUser("Ali"); checkCapacity(); console.log(appName, userCount); // MyApp 2
MyApp: "Sara" added. Total: 1
MyApp: "Ali" added. Total: 2
✅ 98 spots remaining
MyApp 2Local Scope (Function Scope) — Private Variables Inside Functions
A variable has local scope (also called function scope) when it is declared inside a function. It exists only while that function is running and is completely invisible outside. Once the function finishes executing, the local variable is destroyed and the memory is freed.
Local variables provide privacy and isolation. Two functions can have variables with the exact same name and they will never interfere with each other — because each variable lives in its own separate local scope. This is a critical feature for writing safe, maintainable code.
function calculateTax(salary) { let taxRate = 0.2; // local — only inside calculateTax let tax = salary * taxRate; console.log("Tax Rate: 20% | Tax: " + tax); return salary - tax; } function calculateDiscount(price) { let taxRate = 0.1; // same name — but DIFFERENT local variable! let discount = price * taxRate; console.log("Discount Rate: 10% | Discount: " + discount); return price - discount; } console.log(calculateTax(50000)); // Tax: 10000 → returns 40000 console.log(calculateDiscount(2000)); // Discount: 200 → returns 1800 // These two taxRate variables NEVER conflict! π // console.log(taxRate); ← ReferenceError — does not exist outside ✅
Tax Rate: 20% | Tax: 10000
40000
Discount Rate: 10% | Discount: 200
1800Block Scope — let and const Stay Inside { } Blocks
Block scope was introduced with ES6 (2015) through let and const. A variable has block scope when it is declared inside a pair of curly braces { } — such as inside an if statement, a for loop, a while loop, or any standalone block. Variables declared with let or const stay inside their block. Variables declared with var completely ignore block boundaries and leak out.
// BLOCK SCOPE — let/const stay inside { }, var leaks out if (true) { let blockLet = "I am block scoped with let"; const blockConst = "I am block scoped with const"; var blockVar = "I LEAK outside with var!"; console.log(blockLet); // ✅ works inside the block console.log(blockConst); // ✅ works inside the block } console.log(blockVar); // ✅ var leaks: "I LEAK outside with var!" // console.log(blockLet); ← ReferenceError — let respects block scope ✅ // console.log(blockConst); ← ReferenceError — const respects block scope ✅ // Block scope in for loops — critical difference! for (let i = 0; i < 3; i++) { console.log("let i: " + i); // 0, 1, 2 — i exists only in loop } // console.log(i); ← ReferenceError — let i is block scoped ✅ for (var j = 0; j < 3; j++) { console.log("var j: " + j); } console.log("After loop, var j is: " + j); // 3 — var leaked out!
I am block scoped with let
I am block scoped with const
I LEAK outside with var!
let i: 0 let i: 1 let i: 2
var j: 0 var j: 1 var j: 2
After loop, var j is: 3let for loop counters and inside all conditional blocks.var vs let vs const — Complete Scope Comparison
| Feature | var | let | const |
|---|---|---|---|
| Scope type | Function scoped | Block scoped | Block scoped |
| Leaks from if/for blocks? | ✅ Yes — leaks! | ❌ No — contained | ❌ No — contained |
| Re-declarable? | Yes (dangerous) | No | No |
| Hoisted as | undefined | TDZ error | TDZ error |
| Reassignable? | Yes | Yes | No |
| Use in 2026? | ❌ Avoid always | ✅ When value changes | ✅ Default choice |
What is Hoisting? — JavaScript Moves Declarations Up
Hoisting is JavaScript's automatic behavior of moving variable and function declarations to the top of their scope before the code runs. This happens during the compilation phase — before a single line of your code actually executes.
The important detail: only the declaration is hoisted — not the value assignment. This is why accessing a var variable before its line returns undefined (not an error). And this is why you can call a function declared with the function keyword before it appears in your code — the entire function was hoisted.
// ✅ Function DECLARATIONS are FULLY hoisted sayHello(); // Works! Called BEFORE it is defined below function sayHello() { console.log("Hello! I was hoisted! π"); } // var — declaration hoisted, but value is NOT console.log(score); // undefined — hoisted but not yet assigned var score = 100; console.log(score); // 100 — now it has the value // What JavaScript actually sees after hoisting: // var score; ← moved to top (declaration only) // console.log(score); ← undefined // score = 100; ← assignment stays in original place // ❌ Arrow function / Function EXPRESSION — NOT hoisted // greet(); ← TypeError: greet is not a function const greet = () => console.log("Hi from arrow! π"); greet(); // ✅ Works only AFTER the definition // Class declarations — also NOT fully hoisted (TDZ) // const p = new Person(); ← ReferenceError class Person { constructor(n) { this.name = n; } } const p = new Person("Sara"); console.log(p.name); // Sara ✅
Hello! I was hoisted! π
undefined
100
Hi from arrow! π
SaraTemporal Dead Zone (TDZ) — The Safe Guard for let and const
The Temporal Dead Zone (TDZ) is the region of code between the start of a block scope and the actual declaration line of a let or const variable. During this zone, the variable exists in memory (it was hoisted) but is not initialized — any attempt to access it throws a ReferenceError: Cannot access before initialization.
The TDZ is intentionally designed as a safety feature. It prevents the confusing behavior of var (which silently returns undefined when accessed before assignment). With let and const, JavaScript throws a clear error, telling you exactly what went wrong — making your bugs much easier to find and fix.
// var — NO TDZ, returns undefined (misleading but no error) console.log(a); // undefined — no error, just confusing var a = 5; console.log(a); // 5 // let — TDZ active from block start to declaration line // console.log(b); ← ReferenceError: Cannot access 'b' before initialization let b = 10; console.log(b); // ✅ 10 — safe after declaration // const — same TDZ protection as let // console.log(PI); ← ReferenceError: Cannot access 'PI' before initialization const PI = 3.14159; console.log(PI); // ✅ 3.14159 // TDZ visual — inside a block { // ← TDZ for 'city' starts here (variable exists in memory but not usable) // console.log(city); ← ReferenceError — still in TDZ! let city = "Lahore"; // ← TDZ ends here — city is now initialized console.log(city); // ✅ "Lahore" } // TDZ in a function function testTDZ() { // console.log(x); ← ReferenceError — TDZ for x let x = 42; console.log(x); // ✅ 42 } testTDZ();
undefined 5
10 3.14159
Lahore
42var silently returns undefined, you have no idea something went wrong. When let/const throw a ReferenceError in the TDZ, you immediately know there is a problem and exactly where it is. The TDZ catches bugs early and makes debugging much easier.Execution Context — How JavaScript Prepares and Runs Code
An execution context is the environment in which JavaScript code is evaluated and executed. Every time JavaScript runs any piece of code, it creates an execution context for it. Understanding execution context explains why hoisting works the way it does.
There are two types of execution contexts: the Global Execution Context (created once when your script loads — this is where all global variables live) and a Function Execution Context (created every single time a function is called — it has its own local variables and its own this).
Each execution context goes through two phases. In the Creation Phase, JavaScript scans the code, allocates memory for all variables (var set to undefined, let/const placed in TDZ), and stores complete function declarations. In the Execution Phase, the code actually runs line by line and variables get assigned their real values.
window in browser). All global variables live here throughout the program's lifetime.this, and its own local variables. Destroyed when function finishes.Phase 1 (Creation): Allocate var as undefined · let/const go to TDZ · Store full function declarations
Phase 2 (Execution): Run code line by line · Assign real values to variables · Call functions (creates new context)
Common Scope & Hoisting Mistakes — And How to Avoid Them
πΌ Top Interview Questions — Scope & Hoisting in JavaScript
✅ Complete Summary — Scope & Hoisting in JavaScript
⚙️ Local Scope: Declared inside a function — private to that function only
π¦ Block Scope: let/const inside { } — stays inside, never leaks out like var does
π‘ var scope: Function scoped — leaks from blocks, hoisted as undefined, avoid in 2026
π΅ let/const scope: Block scoped — contained, TDZ before declaration, preferred always
π️ Hoisting: Declarations move to top before execution — only declaration, not value
π TDZ: let/const inaccessible before declaration — ReferenceError protects you
⭐ Execution Context: Creation phase (hoisting) → Execution phase (code runs) — explains everything
Scope and hoisting are top interview topics for every JavaScript job. The most practical rules to remember: always declare variables before using them, always use const by default and let when the value changes, never use var, and remember that only function declarations — not arrow functions or function expressions — are fully hoisted. Master these rules and you will avoid 95% of all scope-related bugs!
π» for more posts
- π history of JavaScript
- π variables in JavaScript
- π data types in JavaScript
- π Functions in JavaScript
- π Conditional statements in JavaScript
- π objects in JavaScript
- π loops in JavaScript
- π operators in JavaScript
- π DOM Manipulation in JavaScript
- π What is DOM in JavaScript
Comments
Post a Comment