Understanding Closures
A closure is a function that remembers the variables in the scope where it was created, even after that scope is gone. They're fundamental to functional patterns, callbacks, and module design.
Published May 5, 2026To understand closures, you first need to understand lexical scope: where a variable is defined determines where it can be accessed. In most languages, a function defined inside another function can access the outer function's variables. A closure is what happens when an inner function is returned or passed somewhere else — it "closes over" the variables from its defining scope and carries them with it.
A simple closure in JavaScript
function makeCounter() {
let count = 0; // local to makeCounter
return function() { // inner function — a closure
count += 1;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
After makeCounter() returns, its local scope would normally be garbage collected. But because the returned function still references count, the variable lives on. Each call to counter() reads and updates the same count variable. The closure is the combination of the function and the environment it captured.
Closures in Python
def make_multiplier(factor):
def multiply(x):
return x * factor # closes over 'factor'
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
Each call to make_multiplier creates a new closure with its own captured factor. double and triple are independent functions with independent copies of the outer scope.
Practical uses of closures
Event handlers and callbacks. In JavaScript, event handlers are closures that capture variables from the scope where they're registered:
function setupButton(label, url) {
const btn = document.getElementById(label);
btn.addEventListener('click', function() {
// This function closes over 'url'
window.location.href = url;
});
}
setupButton('home-btn', '/');
setupButton('docs-btn', '/docs/');
Factory functions. Closures let you create specialized functions from general ones, as in the multiplier example above.
Data privacy without classes. Closures can encapsulate state in a way that's inaccessible from outside:
function createWallet(initial) {
let balance = initial; // private to the closure
return {
deposit(amount) { balance += amount; },
withdraw(amount) {
if (amount > balance) throw new Error("Insufficient funds");
balance -= amount;
},
getBalance() { return balance; }
};
}
const w = createWallet(100);
w.deposit(50);
console.log(w.getBalance()); // 150
// balance is not directly accessible: w.balance is undefined
The classic loop pitfall
The most common closure bug in JavaScript involves creating closures in a loop:
// Broken: all functions print 3
const funcs = [];
for (var i = 0; i < 3; i++) {
funcs.push(function() { console.log(i); });
}
funcs[0](); // 3 — not 0!
funcs[1](); // 3
funcs[2](); // 3
The problem: var is function-scoped, so all three closures share the same i variable. By the time the functions run, the loop has finished and i is 3. There are two fixes:
// Fix 1: use let (block-scoped, creates a new binding per iteration)
for (let i = 0; i < 3; i++) {
funcs.push(function() { console.log(i); });
}
// Fix 2: use an IIFE to capture the current value
for (var i = 0; i < 3; i++) {
funcs.push((function(j) {
return function() { console.log(j); };
})(i));
}
Python's late binding in closures
Python has a similar but subtly different trap. Closures in Python capture the variable by reference, not by value. In loops:
# Broken in Python too — all return 2
funcs = []
for i in range(3):
funcs.append(lambda: i)
print(funcs[0]()) # 2, not 0
# Fix: capture the current value with a default argument
funcs = []
for i in range(3):
funcs.append(lambda x=i: x)
print(funcs[0]()) # 0
Memory considerations
Closures keep the captured variables alive for as long as the closure itself is alive. If a closure captures a large object and lives a long time (e.g., as an event listener that's never removed), the captured object is never garbage collected. This is a common source of memory leaks in JavaScript applications. The fix is to remove event listeners when they're no longer needed, or restructure to avoid capturing large objects.
Closures are not complicated once you accept that a function can carry its defining environment with it. Most of the pitfalls come from mutability — closures capturing a variable that changes, rather than the value at capture time. Knowing the difference is enough to use closures fluently.