JavaScript Closures
A closure is what you get when a function "remembers" the variables from the place it was defined, even after that outer code has finished running. Closures are not a special syntax you opt into — they happen automatically, every time you create a function inside another function in JavaScript. Understanding closures is essential because they power private variables, function factories, callbacks, event handlers, and even React hooks like useState.
Overview: How Closures Work
Every time a function is created in JavaScript, it keeps a hidden link to the lexical environment in which it was defined — the set of variables that were in scope at that point in the source code. This link is stored internally (engines expose it informally as [[Environment]]). A closure is simply the combination of a function plus that remembered environment.
This matters because JavaScript uses lexical scoping: a function’s access to variables is determined by where it was written in the code, not by where or how it is later called. When an outer function runs, the JavaScript engine creates a variable environment to hold its local variables and parameters. Normally, once the function returns, that environment would be garbage collected because nothing needs it anymore. But if an inner function was created inside it and that inner function is returned, stored, or passed elsewhere, the inner function keeps a live reference to the outer environment. The garbage collector sees that reference and keeps the environment alive — even though the outer function has already finished executing.
This is why closures feel like "memory": the inner function can still read and even mutate variables that logically belong to a function call that is long over. Each call to the outer function creates a brand-new environment, so each closure returned from a separate call gets its own independent set of variables, unless you deliberately share state across them.
Syntax
There is no special keyword for closures — the pattern is just: define a function inside another function, and let the inner function reference the outer function’s variables.
function outerFunction(outerParam) {
let outerVariable = outerParam;
function innerFunction() {
// innerFunction "closes over" outerVariable
return outerVariable;
}
return innerFunction;
}
| Part | Meaning |
|---|---|
outerFunction |
The enclosing function whose scope will be captured. |
outerVariable |
A variable local to outerFunction that the inner function references. |
innerFunction |
The function defined inside — this is the closure once it escapes the outer scope. |
return innerFunction |
Passing the inner function out keeps its captured environment alive. |
Examples
Example 1: A Simple Counter
function createCounter() {
let count = 0;
return function () {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter());
console.log(counter());
console.log(counter());
Output:
1
2
3
Each call to counter() increments the same count variable, because the returned function is the same closure every time, still pointing at the one count created when createCounter() ran. There is no way to reach count from outside — it is private, accessible only through the function that closes over it.
Example 2: Private State with a Bank Account
function createBankAccount(initialBalance) {
let balance = initialBalance;
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));
console.log(account.withdraw(30));
console.log(account.getBalance());
console.log(account.balance);
Output:
150
120
120
undefined
All three methods returned from createBankAccount close over the same balance variable, so they stay in sync with each other. Notice account.balance is undefined — the balance was never attached to the returned object as a property, so the only way to read or change it is through the methods that were defined alongside it. This is closures used for genuine encapsulation, similar to private fields in a class.
Example 3: Function Factories and Per-Iteration Bindings
function multiplierFactory(factor) {
return (number) => number * factor;
}
const double = multiplierFactory(2);
const triple = multiplierFactory(3);
console.log(double(5));
console.log(triple(5));
const fns = [];
for (let i = 0; i < 3; i++) {
fns.push(() => i);
}
console.log(fns.map((fn) => fn()));
Output:
10
15
[ 0, 1, 2 ]
double and triple are two separate closures, each capturing its own factor from its own call to multiplierFactory. In the loop, because i is declared with let, JavaScript creates a fresh binding of i for every iteration, so each arrow function pushed into fns closes over its own snapshot of i rather than one shared variable.
Under the Hood: Step by Step
- 1. Outer call begins. When
createCounter()(or similar) is invoked, the engine creates a new execution context with its own variable environment holdingcount. - 2. Inner function is created. The anonymous function literal is instantiated. As part of creating any function, the engine attaches a reference to the current lexical environment — the one containing
count. - 3. Outer function returns. The outer call’s execution context is popped off the call stack. Normally its variable environment would now be eligible for garbage collection.
- 4. The reference survives. Because the returned inner function still holds a reference to that environment, the garbage collector cannot reclaim it — it is still reachable.
- 5. Later calls use the live environment. Every time you call the closure, it looks up
countnot in its own (empty) local scope, but by walking the scope chain up to the remembered outer environment, reading and mutating the same variable that persisted from step 1. - 6. Garbage collection eventually happens. Only once nothing references the closure (and nothing references the environment through any other closure) can the engine free that memory.
Common Mistakes
Mistake 1: Using var Inside a Loop
Because var is function-scoped, not block-scoped, every iteration of a loop shares the exact same variable. All closures created in the loop end up pointing at its final value.
const fns = [];
for (var i = 0; i < 3; i++) {
fns.push(function () {
return i;
});
}
console.log(fns.map((fn) => fn()));
Output:
[ 3, 3, 3 ]
By the time any of the pushed functions run, the loop has finished and i is 3 for all of them, because there was only ever one i. The fix is to switch to let, which creates a new binding per iteration:
const fns = [];
for (let i = 0; i < 3; i++) {
fns.push(function () {
return i;
});
}
console.log(fns.map((fn) => fn()));
Output:
[ 0, 1, 2 ]
Mistake 2: Putting Mutable State in the Wrong Scope
A closure only gives you independent, private state if the variable actually lives inside the function that creates the closure. Declaring it one level too high causes every "instance" to secretly share the same value.
let sharedCount = 0;
function createCounter() {
return function () {
sharedCount++;
return sharedCount;
};
}
const counterA = createCounter();
const counterB = createCounter();
console.log(counterA());
console.log(counterB());
console.log(counterA());
Output:
1
2
3
counterA and counterB were meant to be independent, but because sharedCount is declared outside createCounter, both closures point at the exact same variable. Moving the variable inside the factory fixes it:
function createCounter() {
let count = 0;
return function () {
count++;
return count;
};
}
const counterA = createCounter();
const counterB = createCounter();
console.log(counterA());
console.log(counterB());
console.log(counterA());
Output:
1
1
2
Now each call to createCounter creates a fresh count, so counterA and counterB track independently.
Best Practices
- Use closures deliberately for encapsulation — prefer a factory function returning private state over relying on a naming convention like a leading underscore.
- Default to
letandconstovervarso loops and blocks get correct per-iteration bindings automatically. - Be aware that closures keep their entire captured environment alive, not just the variables they use — avoid capturing large objects, arrays, or DOM nodes longer than necessary, and set references to
nullwhen you are done with them in long-lived closures. - Use closures to build configurable, reusable functions via factories and partial application (e.g.
multiplierFactory,once, memoization helpers). - Remember that every call to an outer function produces a brand-new closure with its own copy of the captured variables — state is not shared unless you explicitly place it in a scope common to multiple closures.
- In event listeners and timers, double-check which variables a callback closes over; stale closures (capturing an old value) are a frequent source of bugs in UI code.
Practice Exercises
- Exercise 1: Write a function
makeMultiplier(factor)that returns a function taking one number and returning that number multiplied byfactor. CreatetimesFivefrom it and confirmtimesFive(6)returns30. - Exercise 2: Write a function
once(fn)that returns a new function which only callsfnthe first time it is invoked, caching and returning the same result on every later call, no matter what arguments are passed. - Exercise 3: Given an array of three functions created inside a
for (var i = 0; ...)loop that all incorrectly return the same final value, rewrite the loop so each function returns its own iteration’s index instead, without changing anything outside the loop.
Summary
- A closure is a function bundled together with the lexical environment it was defined in.
- Functions in JavaScript are lexically scoped: they resolve variables based on where they were written, not where they are called from.
- Returning or storing an inner function keeps its outer environment alive, even after the outer function has finished running.
- Each invocation of an outer function creates a new, independent environment, so closures from separate calls don’t share state unless the state is deliberately declared in a common outer scope.
- Closures are the mechanism behind private variables, function factories, and the module pattern.
let/constcreate a new binding per loop iteration, avoiding the classicvar-in-a-loop closure bug.- Because closures keep their environment alive, be mindful of what large data they capture to avoid unnecessary memory retention.
