A closure is a function that "remembers" the variables from the scope it was created in, even after that outer function has finished executing. This sounds abstract until you see it in action — closures are what let you build private state, counters, and memoized functions in JavaScript, and they show up constantly once you start reading other people's code.
A Basic Closure
function makeGreeter(greeting) {
// 'greeting' lives inside makeGreeter's scope
return function (name) {
// this inner function 'closes over' greeting, even after
// makeGreeter has already returned
return `${greeting}, ${name}!`;
};
}
const sayHello = makeGreeter("Hello");
const sayHowdy = makeGreeter("Howdy");
console.log(sayHello("Maya"));
console.log(sayHowdy("Maya"));
Output:
Hello, Maya!
Howdy, Maya!
By the time sayHello("Maya") runs, makeGreeter has long since returned. Normally, a function's local variables disappear once it finishes — but because the inner function references greeting, JavaScript keeps that variable alive in memory for as long as the inner function might still need it. Each call to makeGreeter creates its own separate closure, which is why sayHello and sayHowdy don't interfere with each other.
The Classic Counter Example
Closures are the standard way to create private state in JavaScript — data that can only be changed through specific functions, not accessed directly from outside.
function createCounter() {
let count = 0; // private — not accessible from outside this function
return {
increment() {
count++;
return count;
},
reset() {
count = 0;
},
};
}
const counter = createCounter();
console.log(counter.increment());
console.log(counter.increment());
console.log(counter.increment());
counter.reset();
console.log(counter.increment());
Output:
1
2
3
1
There's no way to directly read or overwrite count from outside createCounter — the only access is through increment() and reset(), both of which share the same closure over the same count variable. This pattern is a lightweight alternative to using a full class when all you need is a bit of encapsulated state.
Why This Matters in Practice
Closures aren't just a theoretical curiosity — they're the mechanism behind debounce/throttle utilities, memoization caches, and module patterns. See a closure put to real use in the debounce function example, where the closure keeps track of a timer between calls. Understanding closures also makes the earlier scope and hoisting tutorial click into place — a closure is really just scope that outlives the function call that created it.
