The definition interviewers want
A closure is a function bundled together with the variables from the scope where it was created. The function keeps access to those variables even after the outer function has returned.
In lesson 1-1 the inner function ran while the outer one was still running. The surprise is that it also works after.
If outer returns inner, the returned function still carries outer's variables with it. They do not get thrown away, because something still holds a reference to them.
That is the mechanical answer to why closures exist at all. JavaScript's garbage collector frees what nothing can reach, and a returned function can reach its birth scope, so that scope stays alive.
Closures are not interview trivia. Every event handler that remembers which button it belongs to, every debounced search box, and every React hook works this way.
The shared need is the same in all three cases. A function has to remember something between calls without a global variable, and a closure is the tool the language provides for exactly that.
A counter that survives its maker
makeCounter finished long before next is called.
function makeCounter() { let count = 0; return function () { count += 1; return count; }; } const next = makeCounter(); console.log(next()); console.log(next()); console.log(next());
Output
1 2 3
count is still alive after makeCounter returned, because the returned function closed over it.
The counting proves the variable is shared rather than recreated. If each call to next got a fresh count, every line would print 1.
Nothing outside can touch count. There is no next.count and no way to set it to 100, which makes this genuinely private state rather than a naming convention.
The variable is let rather than const because it is reassigned. const count = 0 would throw a TypeError on the first count += 1.
Each call creates a fresh scope
Every time you call makeCounter(), JavaScript creates a brand-new count. Two counters made by two calls share nothing.
Two facts explain the behavior.
- The closure captures the variable itself, not a copy of its value.
- Different calls to the maker function capture different variables.
The first point is what makes the counter increment at all, since a copied value could not be updated.
The second is what keeps two counters independent, and together they are the whole model. One variable per call, shared by every closure created during that call.
This is how you get private, per-instance state without classes, and it is the pattern behind the module idiom in the next lesson.
Two counters, two variables
a and b come from separate calls.
function makeCounter() { let count = 0; return function () { count += 1; return count; }; } const a = makeCounter(); const b = makeCounter(); console.log(a()); console.log(a()); console.log(b());
Output
1 2 1
Each has its own private count, so a reaching 2 has no effect on b, which starts from its own zero.
There is one makeCounter function in the source and two separate scopes at runtime. The function body is shared, and the variables it declares are not.
If two closures were created inside the same call, they would share. Returning an object with increment and reset methods gives two functions and one count, which is exactly what lesson 1-3 builds on.
The mental test is to ask how many times the outer function was called. That number is the number of independent states.
The first b() returns 1.
Each call to makeCounter creates a brand-new count variable, so a and b closed over different ones.
a's five calls only touched a's count, leaving it at 5, and nothing about that is visible from b.
b starts from its own count = 0, increments once, and returns 1.
The general rule to carry away is that state lives per call rather than per function. A single makeCounter in the source can back any number of independent counters.
makeAdder
A classic interview warm-up, returning a function that remembers its addend.
function makeAdder(n) { return function (x) { return x + n; }; } const add5 = makeAdder(5); console.log(add5(3)); console.log(add5(10));
Output
8 15
makeAdder returns a function, exactly as makeCounter did, and the returned function closes over n.
The captured variable here is a parameter rather than a local declaration, and parameters are closed over the same way. That is worth noticing, because it is the mechanism behind most real closures.
add5 keeps working after makeAdder returned, so both calls see n as 5.
Nothing is mutated in this one, which makes it the gentlest possible example. The closure is used purely to remember a value, not to accumulate state.
The pattern has a name worth knowing. Turning a two-argument function into a chain of one-argument functions is called currying, and makeAdder is its smallest form.
Run-once functions
Here is a production pattern built from nothing but a closure.
Some actions must happen at most once, such as initializing an SDK, charging a card, or attaching a global listener. Running them twice is a real bug rather than wasted work.
A global alreadyRan flag would work, and it is fragile. Any code anywhere can flip it back, and nothing in the language stops it.
Hiding the flag in a closure fixes that. once(fn) returns a wrapped function whose private called flag no outside code can reach or reset.
The privacy is enforced by the language rather than by convention. There is no property to access, no name to guess, and no way to reach the variable except through the wrapper itself.
Libraries ship this exact function, and being able to write it from memory in five lines is a reasonable interview expectation.
once
The first call runs, and every later call does nothing.
function once(fn) { let called = false; return function (x) { if (called) return undefined; called = true; return fn(x); }; } const init = once((n) => n * 2); console.log(init(5)); console.log(init(5));
Output
10 undefined
The shape is makeCounter again. Declare a closed-over variable, then return a function that reads and updates it.
Setting called = true before calling fn is the detail that matters. If fn throws or calls back into the wrapper, the flag is already set and the guard still holds.
Returning undefined on later calls is a design choice worth naming. Real implementations often cache the first result and return it again, which is friendlier and takes one more variable.
init is an arrow function passed as fn, and it gets closed over just like called, since parameters and locals are captured identically.
The wrapper takes one parameter here for simplicity. A production version would use (...args) and fn(...args) so it wraps any function, which is lesson 8-2's rest-and-spread syntax.