JavaScript Scope and Closures: Lexical Scope Explained with Runnable Examples
Learn to trace JavaScript bindings through global, function and block scope. Build independent closures, repair the classic var loop bug and check your understanding with exact outputs.
KnowledgeGate Team
Exam prep & CS education

Nested functions resolve names from where they are defined, not from who calls them. Block boundaries can remove a binding from view, while a returned function can keep a defining environment reachable after the outer call finishes. Trace each environment and predict every output before running the code.
1. JavaScript scope decides where a binding can be read
Scope determines where a binding can be accessed. Lexical scope fixes lookup from the structure of the written code. JavaScript uses global, function and block environments.
Braces form blocks for let and const. A var belongs to the surrounding function, not an if or for block. Run this trace:
const topic = "global";
function showResult(score) {
const topic = "scope";
if (score >= 60) {
let result = "pass";
var attempts = 2;
console.log(topic, result, attempts);
}
console.log(topic, attempts);
// console.log(result); // ReferenceError if uncommented
}
showResult(72);
console.log(topic);The global environment has topic: "global". The call creates score: 72, shadowing topic: "scope" and function-scoped attempts: 2; the block creates result: "pass". It prints scope pass 2, scope 2, then global. After the block, attempts remains available but result does not.
Declaration choice, temporal-dead-zone behaviour, const-object mutation and loop-binding rules are worked through in JavaScript Variables: let, const and var Scope with Runnable Examples. Closure tracing asks a different question: which environment owns a name, and which bindings remain reachable?

2. Place let, const and var in the correct environment
A let or const declared inside braces belongs to that block. A var declared there belongs to the surrounding function. Lexical lookup can use a binding in the current environment or follow outward links, but it never searches an inner or sibling environment.
Setup-time states and early-access errors are worked through in Hoisting in JavaScript: Predict Output with var, let, const and Functions. Closure analysis starts with a narrower operation: place each declaration in its block, function or global environment before resolving any name.
3. The scope chain follows code structure, not the caller
Identifier resolution walks outwards from the current environment to its enclosing environments and global scope, stopping at the first match. A nearer binding shadows, rather than mutates, an outer name.
const value = 10;
function readValue() {
return value;
}
function run() {
const value = 99;
return readValue();
}
console.log(run()); // 10readValue was defined globally, so value resolves to global 10. Calling it inside run, beside value: 99, does not change its lexical parent. Trace from the function's definition site, not its call site.
4. A closure connects a function to its lexical environment
A closure is a function with access to bindings surrounding its definition. A reachable inner function can still read and update them after the outer call returns. It retains live bindings, not frozen copies of their values.
function makeQuizSession(limit) {
let answered = 0;
return function record(count = 1) {
answered = Math.min(answered + count, limit);
return `${answered}/${limit}`;
};
}
const morning = makeQuizSession(5);
const evening = makeQuizSession(3);
console.log(morning(2)); // "2/5"
console.log(evening()); // "1/3"
console.log(morning(2)); // "4/5"The morning function retains answered and limit from one factory call. Its first call changes answered from 0 to 2, and its next call changes 2 to 4. The evening function reaches a different environment, so its default count changes its own answered value from 0 to 1. The output is 2/5, 1/3, 4/5.
Function syntax, parameters, lexical this and a step-counter factory are compared in Functions and Arrow Functions in JavaScript. The quiz-session closure exposes a different mechanism: two retained environments after both factory calls have returned.
Closures support factories, event handlers and small stateful APIs. Each retained environment stays independent unless the functions deliberately close over the same outer binding.
5. Closure mistakes: shared state, retention and explicit capture
Several callbacks can accidentally share one loop binding. One repair is to create the value that each closure should retain inside the iteration:
const labels = ["alpha", "beta", "gamma"];
const readers = [];
for (var index = 0; index < labels.length; index += 1) {
const capturedLabel = labels[index];
readers.push(() => capturedLabel);
}
console.log(readers.map((read) => read()));
// ["alpha", "beta", "gamma"]The control variable index is function scoped, but each iteration creates a new block-scoped capturedLabel. The callbacks retain those three separate bindings. A let loop variable or a for...of binding provides the same per-iteration separation without the extra constant.
A closure does not copy the whole outer function. Capturing an object can keep it reachable while the callback is reachable. Remove finished listeners or subscriptions, and capture a small field instead of a large object when possible. JavaScript does not promise a garbage-collection time.
Closures and this solve different problems. An arrow can preserve lexical this; an ordinary nested function does not gain outer this merely by closing over local variables.
6. Output questions require environment tracing
Common tasks ask you to trace shadowing, identify temporal-dead-zone access, repair a var loop, draw a scope chain or build independent closure state. Solve this before reading on:
let n = 1;
function outer() {
let n = 5;
return function add(amount = 2) {
n += amount;
return n;
};
}
const addToClosedN = outer();
console.log([
addToClosedN(),
addToClosedN(3),
n
]);The answer is [7, 10, 1]: 5 + 2 = 7, then 7 + 3 = 10. Global n remains 1.
Circle each declaration, draw its environment, connect functions to their definition environments, then update one binding per call in order.
7. Practise scope and closures with three acceptance checks
Shadowing: Set global
let score = 1. Insidepuzzle, set function-scopedlet score = 2; inside anif, set block-scopedlet score = 3. Print in the block, insidepuzzleafter the block, then after callingpuzzle. Expect3, 2, 1.Independent closure state: Implement
makeToggle(initial). One toggle created withfalsemust returntrueand thenfalse. A second toggle created withtruemust returnfalsewithout changing the first toggle's retained state.Definition-site lookup: Set global
const mode = "global"and definereadModebeside it. CallreadModefrom a function whose localmodeis"caller". The result must remain"global"; explain the lexical link that supplies it.
8. JavaScript scope and closures: the short version and next step
Scope limits where a binding can be accessed.
Lexical code structure controls identifier lookup.
The nearest matching binding shadows outer names.
letandconstrespect block boundaries.varrespects the surrounding function boundary.A closure keeps access to live outer bindings.
Rerun showResult(72), makeQuizSession and the explicit-capture loop after changing one value in each. To keep learning the language in order, work through the Complete JavaScript Course. To pair JavaScript with front-end and server-side tools, follow the MERN Stack Course: Full Stack Development.
Your mastery check is concrete: explain why run() returns 10, why morning(2) reaches 4/5, and why each callback retains a different capturedLabel. If you can do that without running the code, you are tracing lexical scope and closure state instead of guessing.
Keep learning

Hoisting in JavaScript: Predict Output with var, let, const and Functions
Build a reliable scope-setup model for JavaScript hoisting, then use it to trace declarations, diagnose three common errors and solve output questions.

CSS Container Queries: Build a React Card That Adapts to Its Container
Build one portable React card that responds to the space its parent provides. Follow its layout, padding and title size at three exact container widths.

Regular Expressions in JavaScript: Match, Extract, Replace, and Validate
Learn JavaScript regex by tracing what each token consumes. Build a validator and a named-group log parser, then practise extraction, replacement, flags, and common traps.

Functions and Arrow Functions in JavaScript: Syntax, Scope and Runnable Examples
Build function declarations, expressions and arrows in JavaScript, then trace parameters, closures, lexical this and array callbacks through exact outputs.

