Overview
A closure is a function that remembers the variables from the scope in which it was created, even after that scope has finished executing. Closures are the mechanism behind callbacks, event handlers, module patterns, and React hooks. This tutorial explains how they work and how to use them without introducing memory leaks or stale data.
Lexical Scope in One Minute
function outer() {
const message = "hello";
function inner() {
console.log(message);
}
inner();
}
outer(); // hello
inner can read message because it was defined inside the scope where message exists. This is lexical scoping: the position of the function in the source code determines what it can access.
Closures Happen When a Function Outlives Its Scope
function makeCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const counter = makeCounter();
counter(); // 1
counter(); // 2
counter(); // 3
makeCounter has finished running, yet the returned function still has access to count. That preserved binding is the closure.
Each Closure Captures Its Own Variable
function createGreeters() {
const names = ["Alice", "Bob", "Carol"];
return names.map(name => () => `Hello, ${name}`);
}
const greeters = createGreeters();
greeters[0](); // Hello, Alice
greeters[1](); // Hello, Bob
Each arrow function captures its own name. This works because arrow functions create a new scope per iteration in map.
The Classic Loop Bug
// Broken with var
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 3, 3, 3
// Correct with let
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 0, 1, 2
var is function-scoped, so all three callbacks share one i. let is block-scoped, so each iteration gets a fresh binding.
Emulating Private Variables
function createBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit(amount) {
if (amount <= 0) throw new Error("Invalid amount");
balance += amount;
return balance;
},
withdraw(amount) {
if (amount > balance) throw new Error("Insufficient funds");
balance -= amount;
return balance;
},
getBalance() {
return balance;
},
};
}
const account = createBankAccount(100);
account.deposit(50); // 150
account.getBalance(); // 150
account.balance; // undefined — not accessible
Before ES2022 class private fields (#balance), closures were the standard way to hide state in JavaScript.
The Module Pattern
const Counter = (() => {
let count = 0;
return {
increment: () => ++count,
reset: () => { count = 0; },
get value() { return count; },
};
})();
Counter.increment();
Counter.increment();
console.log(Counter.value); // 2
Stale Closures in React
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
// BUG: always logs 0 because the closure captured the first render's count
console.log(count);
}, 1000);
return () => clearInterval(id);
}, []); // empty dependency array
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Two fixes:
// Option 1: use a functional update (no dependency on count)
setCount(c => c + 1);
// Option 2: include count in the dependency array
useEffect(() => { ... }, [count]);
Memory Considerations
Closures keep their captured variables alive. If a closure is referenced from a long-lived object, everything it captures stays in memory.
| Pattern | Risk | Mitigation |
|---|---|---|
| Event listener that captures a large DOM subtree | Detached DOM nodes stay in memory | Remove listeners with removeEventListener |
| Interval that captures state | Runs forever, blocks GC | Always clearInterval on cleanup |
| Cache object inside a closure | Unbounded growth | Bound the cache size or use a WeakMap |
Closures vs Classes
| Aspect | Closure | Class |
|---|---|---|
| Syntax | Function returning functions | class with methods |
| Private state | Variables in outer scope | #field syntax |
| Inheritance | Manual composition | Native extends |
| Debugging | Harder to inspect | Named methods in stack traces |
| Memory | Smaller per instance | Methods shared on the prototype |
Both are valid. Use closures for small factories and callbacks; use classes when you need inheritance, or when a large number of instances should share method implementations.
Quick Reference
- A closure is any function that accesses variables from an outer scope, even after that scope has exited.
- Every function in JavaScript forms a closure with its lexical environment.
letandconstcreate per-iteration bindings in loops;vardoes not.- Closures are the foundation of the module pattern, callbacks, and React hooks.
- Always clean up timers and listeners to avoid retaining captured state.
