Scope
Understand what scope is, the different kinds of scope in JavaScript (global, function, block), how lexical scoping works, and how the scope chain resolves variable lookups.
Scope determines where in your code a variable, function, or other identifier is visible and accessible. Every time you declare a variable, JavaScript decides — based on where and how you declared it — which parts of your program are allowed to see it. Understanding scope is essential for avoiding bugs like accidentally overwriting a variable, or wondering why a variable is "not defined" when you know you declared it somewhere.
Global scope
A variable declared outside of any function or block lives in the global scope. It is accessible from anywhere in your program — any function, any block, any file that shares the same global environment (such as multiple <script> tags on one HTML page).
Global scope is convenient but dangerous at scale: the more variables you put in the global scope, the more likely two unrelated pieces of code will accidentally use the same name and overwrite each other's data. This is called global namespace pollution, and it is one of the main reasons modern JavaScript favors modules and small, tightly scoped functions over sprawling global code.
Function scope
Every function creates its own scope. Variables declared inside a function with var, let, or const are only visible inside that function — they do not leak out, and code outside the function cannot see or use them.
This was historically the only kind of local scope JavaScript had (before ES6, only var existed, and var is function-scoped, not block-scoped). If you declare var inside an if block, it is still visible throughout the entire enclosing function, not just inside that if block — a common source of confusion for beginners.
Block scope
ES6 introduced let and const, which are block-scoped: they are only visible inside the nearest pair of curly braces { } that contains them — whether that is an if statement, a for loop, a while loop, or a standalone block.
Block scope is more predictable and safer than function scope because it matches what most developers intuitively expect: a variable declared inside { } should not exist once you leave those braces. This is why modern JavaScript style guides recommend let and const over var almost universally.
• var — function-scoped (ignores block boundaries like if and for) • let — block-scoped, can be reassigned • const — block-scoped, cannot be reassigned after initialization
Lexical scope
JavaScript uses lexical scoping (also called static scoping), which means a variable's scope is determined by where it is physically written in the source code, not by how or when the function is called. When you write a function, its access to outer variables is fixed at the moment you write it — based on its surrounding code — regardless of where that function is later invoked from.
In practice, this means an inner function can always see the variables of the function(s) it is nested inside, because that nesting is visible right there in the code. This is the foundation that makes closures (the next lesson) possible.
The scope chain
When JavaScript needs to resolve a variable name, it does not just look in the current scope — it walks outward through a chain of enclosing scopes, from the innermost block, out through any enclosing functions, all the way to the global scope, stopping as soon as it finds a match.
If the identifier is never found by the time the chain reaches the global scope, JavaScript throws a ReferenceError: "x is not defined". This lookup process is called the scope chain, and it is one-directional — outer scopes can never see into inner scopes, only the reverse.
Declaring variables in different scopes
Each { } after an if, for, while, or standalone block creates a new block scope for let and const. var would ignore the if block entirely and be visible throughout outer(), which is exactly why let/const are preferred.
let globalVar = "I'm global"; // global scope
function outer() {
let functionVar = "I'm function-scoped"; // function scope
if (true) {
let blockVar = "I'm block-scoped"; // block scope
console.log(globalVar); // OK — found in scope chain
console.log(functionVar); // OK — found in scope chain
console.log(blockVar); // OK — declared right here
}
// console.log(blockVar); // ReferenceError — blockVar does not exist here
}Code Examples
siteName is declared in the global scope, so it is visible inside greet() even though greet() never declared it itself — the function looks outward through the scope chain to find it. Expected result: Welcome to SampleApp console.log("Welcome to " + siteName); reads a variable from an outer (global) scope.
This contrasts var's function scope against let's block scope directly. legacyFlag survives outside the if block because var only cares about the nearest function boundary, while modernFlag never escapes the block it was declared in. Expected result: true undefined console.log(typeof modernFlag); avoids a ReferenceError by using typeof, which safely reports "undefined" for an out-of-scope let.
Common mistake: Relying on var's leaky block behavior is a classic source of bugs — a variable you thought was temporary ends up overwriting something later in the same function.
inner() does not declare its own region, so JavaScript walks the scope chain outward: it checks inner (nothing), then middle (finds "Middle") and stops there — it never even looks at outer or global once a match is found. Expected result: Middle This is called shadowing: each nested region redeclares the name and temporarily hides the outer one for any code inside that scope.
console.log(region); resolves by searching the scope chain from innermost to outermost, stopping at the first match.
let siteName = "SampleApp";
function greet() {
console.log("Welcome to " + siteName);
}
greet();function checkAccess(role) {
if (role === "admin") {
var legacyFlag = true; // function-scoped, leaks out of the if
let modernFlag = true; // block-scoped, stays inside the if
}
console.log(legacyFlag); // true — var ignored the block
console.log(typeof modernFlag); // "undefined" — modernFlag doesn't exist here
}
checkAccess("admin");const region = "Global";
function outer() {
const region = "Outer";
function middle() {
const region = "Middle";
function inner() {
console.log(region); // walks the scope chain: inner has none, checks middle
}
inner();
}
middle();
}
outer();Scope nesting and the scope chain
Scope chain lookup always moves from the innermost scope outward, stopping at the first match. Inner scopes can see outer variables; outer scopes can never see into inner ones.
Global Scope
(siteName, region)
│
├── Function Scope: outer()
│ (region = "Outer")
│ │
│ └── Block Scope: if (...) { }
│ (modernFlag)
│
└── Function Scope: middle()
(region = "Middle")
│
└── Function Scope: inner()
(no local variables — looks outward)
Lookup for `region` inside inner() walks:
inner → middle (found here, stop) → outer → globalImportant Rules
var is function-scoped; let and const are block-scoped.
Lexical scope means a function's access to outer variables is fixed by where it is written in the source, not by where or how it is called.
The scope chain resolves a variable by searching the current scope, then each enclosing scope, out to the global scope, stopping at the first match.
If no scope in the chain has the variable, JavaScript throws a ReferenceError.
Minimize the use of the global scope — prefer function- and block-scoped variables to avoid naming collisions.
Where this shows up
Scope bugs are one of the most common sources of hard-to-find defects in real applications — a variable declared with var inside a loop or conditional silently overwriting a variable with the same name elsewhere in the function. Understanding scope is also the prerequisite for understanding closures, module patterns, and why tools like ESLint warn about var and about shadowed variable names.
for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); } // logs 3, 3, 3 for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); } // logs 0, 1, 2 var is function-scoped, so all three callbacks share the exact same i, which has finished looping (and equals 3) by the time they run. let creates a fresh, block-scoped i for each iteration, so each callback captures its own value.
[Multiple Choice] A variable declared with let inside an if block is accessible: A. Anywhere in the entire program B. Anywhere in the enclosing function C. Only inside that if block D. Only on the next line after it is declared
C. Only inside that if block — let (and const) are block-scoped, so they only exist inside the nearest pair of { } that contains them — in this case, the if block.
[True / False] Lexical scope means a function's access to outer variables depends on where the function is called from at runtime. A. True B. False
B. False — False. Lexical scope is determined by where the function is written in the source code, not by where it is later invoked. This is fixed at the time the function is defined.
Write a function calculateTotal(prices) that declares a total variable, uses a for loop with let to add up every number in the prices array, and returns total. Then explain in a comment why using let for the loop counter (instead of var) matters here.
function calculateTotal(prices) { let total = 0; for (let i = 0; i < prices.length; i++) { total += prices[i]; } return total; } // let block-scopes i to the loop, avoiding accidental reuse // of i elsewhere in the function, and giving each iteration // its own independent binding if a closure were created inside.
Declare total = 0 before the loop, and use total += prices[i] inside the loop body.