We tend to use the words “function” and “closure” interchangeably. It’s a handy approximation, but it hides a distinction that explains a big part of how JavaScript behaves: why a variable “outlives” the function that declared it, why two counters created by the same factory stay independent, or how currying and memoization actually work. A closure is not a function. It’s a function plus something else. Let’s look at exactly what that “something else” is.
A function and its lexical scope
A function encapsulates a series of instructions. But it doesn’t live in a vacuum: every function has a lexical scope, that is, the environment in which its variables are resolved. Within that scope, we can distinguish two categories of variables.
Local variables, called “bound”
These are the variables that come from the function’s arguments or are declared directly in its body. They are bound to the function in the sense that they only exist within its boundary.
// x, y, z are "bound" variables
function add(x, y) {
const z = 1
}
These variables are kept in memory for the duration of the function’s execution, then released, at least in the absence of a closure. Their lifetime is, by default, that of the call.
Non-local variables, called “free”
Conversely, a “free” variable is a variable used by the function but defined in a scope outside its own. The function consumes it without declaring it itself.
function createService() {
let count = 0
return {
// "count" is a "free" variable for incrementCounter
incrementCounter() { count++ }
}
}
Here, count is declared in createService, but it is used by incrementCounter, a nested function. From incrementCounter’s point of view, count is a “free” variable. And it’s precisely around these free variables that everything plays out.
So what is a closure?
The definition fits in a single formula:
A closure = a function + a context.
A closure combines a function with its execution context. This context is an environment that keeps in memory the “free” variables needed to run the function. In other words, a closure holds references to these free variables, so that it can reuse them on a later call to the function, even if the scope that declared them has, in appearance, finished executing.
We say that the closure “captures” these variables. These references don’t float around in the void: they are stored in a context attached to the function.
Let’s take a canonical example:
function outer() {
// "free" variable a, defined in the scope of the "outer" function
const a = 10
return function inner() {
// "a" is captured by the closure
return a
}
}
// "someClosure" is actually a closure that holds the references
// to the "outer" function used from the "inner" function
const someClosure = outer() // Closure (outer) {a: 10}
At the moment outer() returns, its call is over. In a world without closures, a would be released and lost. But inner references a: so the closure captures that variable and keeps it alive. On every execution of someClosure, it’s the closure’s context that is consulted to resolve a and return 10.
This is the essential nuance: inner, as a function, is just a block of instructions. someClosure, as a closure, is that block of instructions accompanied by the environment ({a: 10}) that lets it work outside of its original scope.
How is a closure implemented?
In practice, the implementation of a closure varies across runtimes and languages. But looking “under the hood” of a concrete engine helps dispel the magic.
In V8, the JavaScript engine of Chrome and Node.js, a function is represented by an instance of JSFunction. When a function captures free variables, V8 attaches a Context object to it: this structure stores all the captured variables and survives as long as the closure remains reachable.
This implementation detail has a direct and often counterintuitive consequence: as long as a closure is referenced, its context, and therefore the variables it retains, cannot be collected by the garbage collector. This is what makes closures so powerful for holding state, but it’s also what can lead to memory leaks if you inadvertently capture large objects you no longer need.
What is a closure good for?
Capturing context is not an academic curiosity: it underpins a large part of the patterns we work with daily in JavaScript. A few examples of concepts that rely directly on closures:
- currying;
- partial application;
- callbacks and event listeners;
- memoization;
- higher-order functions.
Each of these mechanisms exploits the same fundamental idea: a function that “remembers” the environment in which it was created. That’s exactly what we’ll explore in the next installment of this series, dedicated to the concrete use cases of closures.
Conclusion
Keep the distinction in mind: a function is a series of instructions endowed with a lexical scope; a closure is that function combined with the context that retains its “free” variables. The function describes what to execute; the context provides what to execute it with, even long after the original scope has disappeared.
Understanding this nuance isn’t quibbling over vocabulary. It’s grasping why state persists, how functional abstractions are built, and where to watch for memory leaks. Once you’ve internalized the mechanics of capture, entire swaths of the language, from currying to callbacks, stop being recipes and become obvious consequences of one and the same principle.