What is a Closure?
How JavaScript functions remember the variables from the scope they were created in.
2 min read
A closure is what you get when a function "remembers" the variables from the scope it was defined in, even after that outer scope has finished running. Every function in JavaScript forms a closure — it's not an opt-in feature, just a natural consequence of how scoping works — but the term is used specifically when that memory is actually put to use.
The classic example
function makeCounter() {
let count = 0;
return function () {
count++;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3makeCounter() runs and returns, and normally you'd expect its local variable count to be gone once the function exits. It isn't. The inner function that makeCounter returns still has a live reference to count, so every call to counter() reads and updates the same variable. That inner function closed over count — hence "closure."
Each call creates a separate closure
Calling makeCounter() again creates an entirely new count and a new closure around it — the two don't share state:
const counterA = makeCounter();
const counterB = makeCounter();
counterA(); // 1
counterA(); // 2
counterB(); // 1 -- independent from counterAWhy closures matter
Closures are how JavaScript gives you private state without classes: count in the example above can't be accessed or modified directly from outside makeCounter — only through the function that closed over it. This pattern shows up constantly:
function createBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit(amount) {
balance += amount;
return balance;
},
getBalance() {
return balance;
},
};
}
const account = createBankAccount(100);
account.deposit(50);
console.log(account.getBalance()); // 150
console.log(account.balance); // undefined -- not accessible directlybalance only exists inside the closure formed by deposit and getBalance — there's no way to reach it except through the methods that were deliberately exposed.
A common gotcha: closures in loops
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Logs: 3 3 3 -- not 0 1 2Because var is function-scoped, all three callbacks close over the same i, which has finished looping (reaching 3) by the time any of them actually run. Switching var to let fixes it — let creates a fresh binding of i for each loop iteration, so each closure captures its own copy:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Logs: 0 1 2This is one more concrete reason let replaced var as the default choice, beyond the scoping rules covered earlier in this course.
Closures depend on lexical scope — where a function is defined. The next lesson covers this, which works the opposite way: determined by how a function is called.