Closures
Understand what a closure is, why it happens as a natural consequence of lexical scoping, and how to use closures to create private state and other practical patterns.
A closure is what happens when an inner function "remembers" the variables from the scope it was created in, even after that outer scope has finished running. Closures are not a special syntax you opt into — they happen automatically, every time you define a function inside another function. Understanding them unlocks private variables, function factories, and a huge share of patterns used in real JavaScript code, including in React hooks and event handlers.
Closure, defined precisely
A closure is the combination of a function and the lexical environment (the set of variables) within which that function was declared. When an inner function is returned from, or passed out of, an outer function, it keeps a live reference to the outer function's variables — not a copy of their values at that moment, but a continuing connection to the actual variables.
This is possible because of lexical scoping (covered in the Scope lesson): the inner function's access to outer variables is fixed by where it was written, so JavaScript keeps that outer scope alive in memory for as long as the inner function might still need it, instead of destroying it the instant the outer function returns.
The lexical environment stays alive
Normally, when a function finishes running, its local variables are cleaned up (garbage collected) because nothing needs them anymore. But if a function creates an inner function that outlives it — for example, by returning that inner function — JavaScript keeps the outer variables that the inner function references alive in memory, specifically because the closure still needs them.
This is why a closure can keep incrementing a counter, keep a secret value hidden, or remember a configuration value, long after the function that originally declared that variable has already returned.
Practical examples
The most common first example of a closure is a counter function: an outer function declares a count variable and returns an inner function that increments and returns it. Every call to the returned function shares the exact same count — it does not reset, because the closure keeps that one variable alive across calls.
Closures also show up constantly in event handlers, timers, and callbacks: a callback function passed to addEventListener or setTimeout closes over whatever variables were in scope where it was defined, and can still use them whenever it eventually fires.
Private state with closures
Because a closure's outer variables are not accessible from outside the closure at all — there is no dot notation, no global name for them — closures are JavaScript's classic mechanism for creating truly private data, before the language had a dedicated private class-field syntax.
A function can expose only a small controlled interface (like increment(), decrement(), and getValue() functions) while keeping the actual data variable completely inaccessible except through those functions. This pattern is often called the module pattern or a "factory function with private state."
• Counters and generators that must remember their last value between calls • Memoization / caching functions that remember previously computed results • Event handlers and callbacks that need access to variables from when they were set up • Data hiding / encapsulation, simulating private variables before # private class fields existed
Basic closure shape
makeCounter() runs once and returns an inner function — but count is not destroyed afterward, because the returned function still references it. Each call to counter() reuses the same count variable; it does not reset to 0 on every call. There is no way to read or set count directly from outside — the only access is through the returned function.
function makeCounter() {
let count = 0; // private to this closure
return function () {
count += 1;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3Code Examples
Each call to greeter(name) creates a brand-new closure with its own separate name variable. greetAda and greetLin are two different inner functions, each remembering a different value of name from its own call to greeter. Expected result: Hello, Ada! Hello, Lin!
return function () { console.log("Hello, " + name + "!"); }; captures name from the specific call to greeter() that created it.
balance is completely private: the only ways to read or change it are the deposit, withdraw, and getBalance methods returned from createBankAccount, each of which closes over the same balance variable. Trying to access account.balance directly fails because balance was never attached to the returned object — it only exists inside the closure. Expected result: 150 120 undefined This is the module pattern: expose a controlled public interface (deposit/withdraw/getBalance) while keeping the underlying data (balance) truly private.
console.log(account.balance); returns undefined because balance lives only in the closure's lexical environment, not as a property of the returned object.
memoize returns a new function that closes over a private cache object. Every call to memoizedSquare checks and updates the same cache, because it's the same closure — not a fresh one created each time. Expected result: computing for 5 25 cache hit for 5 25 computing for 6 36 if (n in cache) {... } checks the closed-over cache before recomputing, turning repeated calls with the same input into fast lookups.
function greeter(name) {
return function () {
console.log("Hello, " + name + "!");
};
}
const greetAda = greeter("Ada");
const greetLin = greeter("Lin");
greetAda();
greetLin();function createBankAccount(startingBalance) {
let balance = startingBalance; // private
return {
deposit(amount) {
balance += amount;
return balance;
},
withdraw(amount) {
if (amount > balance) {
console.log("Insufficient funds");
return balance;
}
balance -= amount;
return balance;
},
getBalance() {
return balance;
}
};
}
const account = createBankAccount(100);
console.log(account.deposit(50)); // 150
console.log(account.withdraw(30)); // 120
console.log(account.balance); // undefined — not accessible directlyfunction memoize(expensiveFn) {
const cache = {}; // private cache, shared across all calls
return function (n) {
if (n in cache) {
console.log("cache hit for", n);
return cache[n];
}
console.log("computing for", n);
const result = expensiveFn(n);
cache[n] = result;
return result;
};
}
function square(n) {
return n * n;
}
const memoizedSquare = memoize(square);
console.log(memoizedSquare(5)); // computes
console.log(memoizedSquare(5)); // cached
console.log(memoizedSquare(6)); // computesHow a closure keeps outer variables alive
The inner function does not copy count's value when it is created — it keeps a live reference to the variable itself, in the exact scope where it was declared.
makeCounter() runs once
┌───────────────────────────┐
│ let count = 0; │ ← lives in makeCounter's
│ │ lexical environment
│ return function () { │
│ count += 1; ──────────┼──► inner function closes over
│ return count; │ │ `count` by reference
│ }; │ │
└─────────────────┼─────────┘
│
▼
counter (returned function)
even though makeCounter() has already
returned, `count` is kept alive because
`counter` still references it.
counter() → count becomes 1 → returns 1
counter() → count becomes 2 → returns 2 (same count each time)Important Rules
A closure forms whenever an inner function is defined inside an outer function and is used (returned, passed as a callback, or stored) outside that outer function's own execution.
A closure captures variables by reference, not by value — if the outer variable changes later, the closure sees the updated value.
Each separate call to the outer function creates a new, independent closure with its own copy of the outer variables.
Closures are the standard way to create private state in JavaScript, since variables in an outer function's scope have no external name or path to access them directly.
Closures keep their referenced variables in memory for as long as the closure itself is reachable — this is normal and intentional, not a memory leak, as long as you eventually let go of the closure when you're done with it.
Where this shows up
Closures power React's useState and useEffect hooks (each render's event handlers close over that render's specific state values), Node.js middleware factories, debounce/throttle utility functions, and any "configure once, call many times" API. Nearly every JavaScript library you'll use relies on closures internally, even when you never write the word "closure" yourself.
for (var i = 1; i <= 3; i++) { setTimeout(function () { console.log(i); }, 100); } // logs 4, 4, 4 for (let i = 1; i <= 3; i++) { setTimeout(function () { console.log(i); }, 100); } // logs 1, 2, 3
With var, all three timeout callbacks close over the exact same i, which has already finished the loop (and become 4) by the time any timeout fires. let creates a fresh, block-scoped i for every iteration, so each closure captures its own independent value.
[Multiple Choice] What is a closure in JavaScript? A. A syntax for closing an HTML tag early B. A function combined with the lexical environment (outer variables) it was defined within, which it keeps access to even after the outer function returns C. A way to permanently lock a variable's value so it can never change D. A built-in method for closing open network connections
B. A function combined with the lexical environment (outer variables) it was defined within, which it keeps access to even after the outer function returns — A closure is a function bundled with references to the variables from its surrounding (lexical) scope at the time it was created, remaining accessible even after the outer function has finished executing.
[True / False] Two separate calls to the same outer function share the exact same closed-over variables. A. True B. False
B. False — False. Each call to the outer function creates a brand-new, independent lexical environment, so each resulting closure has its own separate copy of the outer variables — they do not share state with each other.
Write a function createMultiplier(factor) that returns a new function. That returned function should take a number and return it multiplied by factor. Then create double = createMultiplier(2) and triple = createMultiplier(3), and show that they behave independently.
function createMultiplier(factor) { return function (n) { return n * factor; }; } const double = createMultiplier(2); const triple = createMultiplier(3); console.log(double(5)); // 10 console.log(triple(5)); // 15 // double and triple each closed over their own separate // factor value from their own call to createMultiplier.
The returned inner function needs to reference factor from the outer function's scope — that reference is the closure.