Scope (block, function, global)
Scope is the set of rules that decides which variables a piece of code can see. In JavaScript, scope is determined by where you write the code, not where it ends up being called from — a property called lexical scope. Once you internalise that, most "why can't I see this variable here?" puzzles solve themselves.
The three kinds of scope
Global scope — declared outside any function or block. In a browser, that means attached to
window(forvarand traditionalfunctiondeclarations) or the module level (forlet,const, ES modules).Function scope — every function creates a scope. Anything declared inside is visible only inside, plus any nested functions.
Block scope — every pair of
{ ... }creates a scope forletandconst(andclass).vardoes not respect block scope.
const planet = "Earth"; // global (module-level)
function describe() {
const adjective = "blue"; // function scope
if (planet === "Earth") {
const verdict = "habitable"; // block scope
console.log(`The ${adjective} ${planet} is ${verdict}.`);
}
// console.log(verdict); // ReferenceError — out of scope
}
describe();The blue Earth is habitable.
The scope chain
When code references a name, the engine looks in the current scope first, then the surrounding scope, then the next one out, all the way up to the global scope. The chain stops at the first match.
const a = "global";
function outer() {
const b = "outer";
function inner() {
const c = "inner";
console.log(a, b, c); // each name is found in its nearest scope
}
inner();
}
outer();global outer inner
If the name is never found, you get a ReferenceError. If a name is found in an inner scope, the inner one wins — that's shadowing, covered below.
var function-scoping vs let/const block-scoping
This is the single most important difference between the legacy keyword and the modern ones. var doesn't care about blocks; let and const do.
function example() {
if (true) {
var v = "I leak out";
let l = "I stay";
}
console.log(v); // "I leak out"
console.log(l); // ReferenceError
}
example();This trips up loops in particular. The classic interview-bug:
// var version — every callback shares the same i
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log("var:", i), 0);
}
// let version — each iteration gets its own i
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log("let:", j), 0);
}var: 3 var: 3 var: 3 let: 0 let: 1 let: 2
With var, there is one shared i; by the time the callbacks fire, the loop is finished and i is 3. With let, each iteration creates a fresh j in a fresh block scope, and each closure captures its own.
Global pollution
Globals are visible everywhere — that sounds convenient and is the source of countless bugs. Two scripts that both declare var user on the page collide silently. Any function can mutate a global, so tracking down "who changed this" becomes painful.
// in script-a.js
var config = { theme: "dark" };
// in script-b.js, loaded later
var config = { lang: "en" }; // silently overwrites — bug!IIFE — the old way to make a scope
Before let and modules, the way to get a private scope was to wrap code in an Immediately Invoked Function Expression (IIFE) — a function defined and called in one step.
(function () {
var secret = "shhh";
// anything declared here is invisible outside this function
console.log(secret);
})();
// secret is undefined out hereToday you'd usually just write an ES module or a block with let. IIFEs still appear in bundled scripts and library globals — recognise them, but you rarely need to write a new one.
Shadowing
An inner scope can re-use a name from an outer scope; the inner declaration shadows the outer one for the duration of its block.
const user = "Ada";
function greet() {
const user = "Guest"; // shadows the outer 'user' inside this function
console.log("Hello,", user);
}
greet();
console.log("Outside:", user);Hello, Guest Outside: Ada
Functions, closures and scope
A function "remembers" the scope in which it was defined, not where it is called. That memory is called a closure and we cover it in depth later — but it falls out of the rules on this page:
function makeCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const next = makeCounter();
console.log(next()); // 1
console.log(next()); // 2
console.log(next()); // 3count is private to makeCounter's scope; the returned function still has access to it after makeCounter has returned, because scope is decided lexically.
Rules of thumb
Declare variables as close to where you use them as possible.
Keep scopes small — short functions and tight blocks make bugs obvious.
Avoid mutable globals. If something feels global, it usually wants to be a module export instead.
Treat shadowing as something you do on purpose, not by accident — enable
no-shadowin your linter.