Closures are a fundamental concept in JavaScript that allow functions to access variables from their outer scope. This article explains closures, their practical uses, and common pitfalls.
What Are Closures?
A closure is the combination of a function bundled together with references to its surrounding state. In plain terms: the inner function remembers the variables of its parent, even after the parent has finished executing.
function outerFunction(outerVariable) {
return function innerFunction(innerVariable) {
console.log(`Outer Variable: ${outerVariable}`);
console.log(`Inner Variable: ${innerVariable}`);
};
}
const closureExample = outerFunction("hello from outer");
closureExample("hello from inner");
// Outer Variable: hello from outer
// Inner Variable: hello from inner
A Practical Use Case: Counters
Closures are the natural way to encapsulate private state without a class.
function makeCounter() {
let count = 0;
return {
increment() { count++; },
decrement() { count--; },
value() { return count; },
};
}
const counter = makeCounter();
counter.increment();
counter.increment();
console.log(counter.value()); // 2
count is completely private. There is no way to touch it from outside makeCounter.
Closures in the Wild
You encounter closures every time you write a callback, event handler, or factory function:
// Event handler captures `buttonId` from the loop variable
buttons.forEach((btn, buttonId) => {
btn.addEventListener("click", () => {
console.log(`Button ${buttonId} clicked`);
});
});
Without closures, every handler would print the same final value of buttonId.
Common Pitfall: The Loop Problem
The classic trap — using var inside a loop creates one shared binding:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Prints: 3, 3, 3
Fix it with let (which creates a new binding per iteration) or wrap in an IIFE:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Prints: 0, 1, 2
Memory Considerations
Closures keep their outer scope alive as long as the inner function is reachable. This is usually fine, but watch out for unintentionally long-lived references — for example, storing a closure in a global variable that holds a reference to a large object graph.
Takeaway
Closures are not a trick or an edge case — they are how JavaScript lexical scoping works. Once that clicks, patterns like module encapsulation, currying, and memoization stop feeling magical and start feeling obvious.