Recommended resources
The freely available resources this lesson will direct you to.
Hello again! Last lesson, you traced when a callback runs. This time, we’ll trace which variables it can access when it runs. That distinction matters when a Node callback runs after its creating function has returned, or when several callbacks are created in a loop.
By the end of this lesson, you’ll be able to build a function that keeps its own state without a global variable, explain why two instances do not interfere with each other, and diagnose a common closure-related interview bug.
Lexical scope: where a function is written matters
Consider a nested function:
function makeGreeting(name) {
function greet() {
return `Hello, ${name}`;
}
return greet;
}
const greetSam = makeGreeting("Sam");
console.log(greetSam()); // "Hello, Sam"
greet has no local variable named name, so JavaScript looks in its enclosing scope and finds the parameter of makeGreeting. This is lexical scope: a function’s access to outer variables is determined by where the function is created in the source code, not by where it is eventually called.
There is a crucial detail here. makeGreeting("Sam") returns the function greet; it does not call greet. When greetSam() runs later, it can still access the name belonging to that particular call of makeGreeting. The returned function together with its access to that surrounding environment is a closure. JavaScript creates that connection when it creates the function; returning it simply makes the connection easier to observe.
Watch Dave Gray’s Javascript Closure Tutorial | Closures Explained for the transition from nested scope to a returned function. His example also changes a global variable; watch for the difference between that globally accessible value and the variable local to the outer function.
Javascript Closure Tutorial | Closures Explained
Dave Gray’s demonstration shows why an inner function still works after its parent call finishes.
Watch scope definition and nested functions for where variable lookup begins. Then watch closure definition and returned function. Focus on what changes when the parent returns its child rather than calling it immediately, and on what happens when that returned function is called repeatedly.
A useful way to check your understanding is to separate two questions: Where was this function created? That determines its outer scope. When is it called? That determines when it reads the variables in that scope.
A closure retains a variable, not a frozen value
A closure does not automatically take a snapshot of every value it can see. It retains access to a binding—the variable itself—so it can observe or change that variable later.
function makeCounter() {
let count = 0;
return function next() {
count += 1;
return count;
};
}
const nextId = makeCounter();
console.log(nextId()); // 1
console.log(nextId()); // 2
console.log(nextId()); // 3
Each call to nextId() creates a fresh execution of next, but all three calls find the same count from the earlier makeCounter() call. That variable remains available because the returned function still needs it. It is not global: code outside makeCounter cannot refer to that local count by name.
Now create another counter:
const first = makeCounter();
const second = makeCounter();
console.log(first()); // 1
console.log(first()); // 2
console.log(second()); // 1
console.log(first()); // 3
Every invocation of makeCounter() creates its own local count. first retains access to one binding; second retains access to another. They use the same function definition, but not the same state.
The diagram below shows the same principle with two counter objects. Treat its “scope objects” as a conceptual picture of retained environments, not as ordinary JavaScript objects you can access directly. In particular, its “Global object” label is not a requirement of this pattern: we can store counter instances in module-local variables instead of global properties.

Notice the two kinds of sharing: functions produced by one factory call can share state; functions produced by different factory calls keep their state separate.
Build a small interface around private state
A returned object can expose several functions that share one closure. This is useful when callers need controlled ways to update state as well as read it:
function createCounter(initial = 0) {
let count = initial;
return {
increment() {
count += 1;
return count;
},
get() {
return count;
},
};
}
const requests = createCounter(100);
const jobs = createCounter(200);
console.log(requests.increment()); // 101
console.log(requests.get()); // 101
console.log(jobs.get()); // 200
Within requests, increment and get refer to the same count. Within jobs, they refer to a different one. Neither returned object exposes its count binding as a property. For example, assigning requests.count = 999 would add a property to the object, but requests.get() would still return 101.
Read MDN’s Closures for its counter example and explanation of how multiple returned functions share an environment.
Closures - JavaScript - MDN Web Docs - Mozilla
MDN’s section on private methods develops the same pattern into an interface with multiple operations and independent instances.
In “Emulating private methods with closures,” start at the paragraph beginning “The following code illustrates how to use closures to define public functions.” Read the counter examples through the explanation immediately after counter1 and counter2. Focus on why one counter’s methods share a variable, while two calls to the factory do not. The first example uses an immediately invoked function expression to create one counter; the later makeCounter example is the one to compare with our factory.
“Private” here means callers cannot access the local binding directly. It does not mean the state cannot change: the methods you choose to return determine which changes callers are allowed to make. This is the design value of a closure, not merely a trick for remembering values.
When delayed callbacks read the same binding
The previous lesson established that a timer callback runs later. Closures answer a separate question: what will it read when it finally runs? First, look at the issue without a timer:
function makeReadersWithVar() {
const readers = [];
for (var i = 0; i < 3; i++) {
readers.push(() => i);
}
return readers;
}
const readers = makeReadersWithVar();
console.log(readers.map((read) => read())); // [3, 3, 3]
var i is scoped to the function, not separately to each loop iteration. All three functions refer to the same i. By the time they are called, the loop has finished and that binding holds 3. The functions did not save 0, 1, and 2 at creation time.
A for loop declared with let gives its callbacks distinct iteration bindings:
function makeReadersWithLet() {
const readers = [];
for (let i = 0; i < 3; i++) {
readers.push(() => i);
}
return readers;
}
console.log(makeReadersWithLet().map((read) => read())); // [0, 1, 2]
The same distinction explains a familiar timer question. If each loop iteration schedules setTimeout(() => console.log(i), 0), the callbacks run after the synchronous loop. With var, each reads the final value of one shared binding; with let, each reads its own iteration’s binding. Scheduling determines when the read occurs; lexical scope determines which variable is read.
Return to MDN’s Closures for its form-event-handler version of this bug. Unlike a timer, the callback waits for a user action, but the underlying issue is identical.
Closures - JavaScript - MDN Web Docs - Mozilla
This section makes the shared-binding bug concrete and compares older closure-based fixes with let and const.
In “Creating closures in loops: A common mistake,” begin at the paragraph starting “Prior to the introduction of the” and read the loop example through the paragraph introducing for...of and forEach. The example first attaches onfocus handlers to three fields; then MDN explains why each initially shows the last field’s help text. Compare the function-factory fix, which creates a new parameter binding for each callback, with the modern block-scoped fix.
The practical rule is not “closures in loops are bad.” It is: identify whether callbacks should share one changing variable or each retain a distinct binding. Choose the scope that matches the intended behavior.
Put the pattern to work in a Node-style component
Suppose an application setup function needs to issue request identifiers, and separate application instances should not share a sequence. A factory gives each instance local state:
function createRequestIdGenerator(prefix) {
let nextNumber = 0;
return function nextRequestId() {
nextNumber += 1;
return `${prefix}-${nextNumber}`;
};
}
function createAppTools(prefix) {
const nextRequestId = createRequestIdGenerator(prefix);
return {
handleRequest() {
return { requestId: nextRequestId() };
},
};
}
const api = createAppTools("api");
const admin = createAppTools("admin");
console.log(api.handleRequest()); // { requestId: "api-1" }
console.log(api.handleRequest()); // { requestId: "api-2" }
console.log(admin.handleRequest()); // { requestId: "admin-1" }
nextNumber belongs to a particular generator call. The public handleRequest method can use its generator without exposing the number for arbitrary modification. This is in-memory state for the lifetime of that instance, not a durable or cross-process ID system; a restart or another server process would start its own sequence. The example is about scope and ownership, not production-grade unique IDs.
A concise interview explanation for this design would be: “The factory creates a local binding. The returned function closes over that binding, so calls to one instance update the same state. Calling the factory again creates an independent binding, and callers can interact with it only through the functions we return.”
Key takeaways
Lexical scope determines which outer variables a function can reach based on where it was created. A closure retains that access even when the function is called after its creating call has returned. It retains bindings rather than frozen values: functions from one factory call can share changing state, while separate factory calls have independent state.
When tracing a callback, ask both when will it run? and which binding will it read? Keeping those questions separate explains counter factories, private state, and loop-callback bugs. Next, we’ll examine a different aspect of function calls: how call style and arrow functions affect this.