Closures are often presented as a quirk of the language, a bit of theory you tick off while reading a JavaScript book before moving on to something else. That’s a flawed perspective. Closures aren’t a footnote: they’re the silent mechanism underpinning a large part of the JavaScript you write every day, often without even realizing it.
In the first part of this series, we saw what a closure really is: a function that captures and retains access to its lexical environment, even after the parent function has finished executing. One question remained: what is that actually good for? Here is a list, non-exhaustive, of six fundamental concepts that only hold together thanks to closures. You’re already using most of them.
Currying
Currying consists of transforming a function that takes several arguments into a series of functions, each taking only a single argument. Instead of calling f(a, b, c), you call f(a)(b)(c). Each intermediate function captures, via a closure, the arguments from the previous steps to make them available to the next one.
function multiply(a) {
return function (b) {
return function (c) {
return a * b * c;
};
};
}
multiply(2)(3)(4); // 24
Here, the innermost function still accesses a and b even though the functions that introduced them returned long ago. This is exactly the role of the closure: keeping that lexical context alive from one call to the next. Without it, a and b would have vanished from the stack well before the final multiplication could take place.
Partial application
Partial application lets you create a specialized version of a function by pre-supplying some of its arguments. You get a new, more precise function that now only expects the remaining arguments. There are two classic ways to put it into practice.
Through currying
A curried function naturally performs partial application: calling the parent function returns a function for which the first argument is already supplied and kept in a closure.
function add(a) {
return function (b) {
return a + b;
};
}
const add10 = add(10); // `a` (= 10) is frozen in the closure
add10(5); // 15
add10(20); // 30
add10 is a specialized variant of add: the value 10 is permanently captured in it. Each subsequent call reuses that context without having to specify it again.
Through argument binding
The other path consists of pre-filling the arguments of an existing function. In JavaScript, Function.prototype.bind creates precisely a closure that remembers the context (this) and the arguments supplied in advance.
function greet(greeting, name) {
return `${greeting}, ${name}!`;
}
const sayHello = greet.bind(null, "Hello");
sayHello("Antoine"); // "Hello, Antoine!"
In both cases, the result is the same: you derive a pre-configured version of a generic function, and it’s a closure that retains the already-applied values.
Higher-Order Functions
A higher-order function is a function that takes another function as an argument, returns one, or both. They’re what makes JavaScript code composable. Closures are indispensable to them: they allow the produced function to capture the lexical context defined at the moment of its creation.
function makeMultiplier(factor) {
return (value) => value * factor;
}
const double = makeMultiplier(2);
const triple = makeMultiplier(3);
double(5); // 10
triple(5); // 15
double and triple share the same code but each carries its own factor, captured in a distinct closure. This is the mechanism hiding behind map, filter, reduce and most functions that manipulate other functions.
Callbacks and event listeners
Asynchronous code relies heavily on callbacks and event listeners. Between the moment an action is defined and the moment it actually fires, time passes, sometimes a few milliseconds, sometimes much more. During that interval, the context of the original function must be preserved. That, once again, is the work of a closure.
/**
* 4. CALLBACKS and EVENT LISTENERS
*
* To handle asynchronous code, callbacks and event listeners can be
* used. Most often, these mechanisms rely on Closures to
* preserve the context between the moment the action is defined and the moment it
* is triggered.
*/
function doSomethingAsync(input: number, callback: (result: string) => void) {
const result = `processed ${input} items`;
setImmediate(() => {
callback(result);
});
}
doSomethingAsync(1000, (result) => {
console.log(result); // processed 1000 items
});
When setImmediate finally executes its function, the call to doSomethingAsync has long since finished. Yet result is still accessible: the function passed to setImmediate captured the variable in a closure. Without this mechanism, all asynchronous programming based on callbacks would be impossible.
Memoization
Memoization is an optimization technique that consists of caching the result of a function for a given set of arguments, in order to avoid re-running it needlessly if it’s called again with the same inputs. The cache that stores these results must survive between calls without polluting the global scope: a closure is the ideal place to house it.
function memoize(fn) {
const cache = new Map(); // kept in the closure, private and persistent
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn(...args);
cache.set(key, result);
return result;
};
}
const slowSquare = (n) => {
// imagine an expensive computation here
return n * n;
};
const fastSquare = memoize(slowSquare);
fastSquare(4); // computed then cached
fastSquare(4); // served from the cache, no recomputation
The cache exists nowhere other than in the closure created by memoize. It’s invisible from the outside, specific to each memoized function, and persists as long as the returned function remains referenced.
Encapsulation
Finally, closures let you emulate private state, something JavaScript didn’t offer natively before the arrival of private class fields (#field). By capturing variables in a lexical scope, a closure makes them inaccessible directly from the outside. Only the functions defined in that same scope can read or modify them. This is the foundation of the module pattern.
function createCounter() {
let count = 0; // private variable, inaccessible from the outside
return {
increment() {
count += 1;
return count;
},
decrement() {
count -= 1;
return count;
},
value() {
return count;
},
};
}
const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.value(); // 2
counter.count; // undefined, no way to touch it directly
count is exposed through no direct path: you can only interact with it through the functions returned by createCounter. Closures thereby guarantee genuine encapsulation, where the internal state stays protected and the public interface stays under control.
In summary
Currying, partial application, higher-order functions, callbacks, memoization, encapsulation: six pillars of modern JavaScript that share a single foundation. Each time, the need is identical, retaining access to a lexical context beyond the lifetime of the function that created it, and the answer is the same: a closure.
This is what makes closures far more than a point of theory. Understanding them isn’t just about passing an interview question: it’s about grasping the mechanism that underpins a large part of the patterns you already work with, and gaining the ability to design them yourself with full awareness.