A closure is a function together with a reference to the lexical environment in which it was created, which lets the function read and often modify variables from that enclosing scope even after the outer function has returned. In functional programming, closures are the mechanism that turns functions into first-class values able to carry private state.
Closures make higher-order functions, callbacks, partial application and currying practical. They are native to JavaScript, Python, Ruby, Swift, Kotlin, C#, Go, Rust, Haskell and most other modern languages, and they matter because they decide what data a function captures, for how long, and by value or by reference.
How a closure captures its environment
Closures rely on lexical scoping: a variable name refers to the binding visible where the code is written, not where it is called. When an inner function refers to a variable of its outer function, that variable becomes a free variable of the inner function, and the runtime must keep it alive as long as the inner function exists.
Implementations do this by moving captured variables out of the call stack into a heap-allocated environment. JavaScript engines create a context object, Python stores captured variables in cell objects listed in the function’s __closure__ attribute, and C# compilers generate a hidden class whose fields hold the captured variables. The function value then becomes a pair: code pointer plus environment.
Because the environment is shared, several closures created in the same call see the same variables. A counter factory that returns both an increment and a read function works precisely because both closures reference one shared binding rather than separate copies.
Capture semantics across languages
The most consequential detail of closures is whether they capture a variable or its value, and languages answer this differently. The table summarizes the behavior in widely used languages.
| Language | Capture default | Can modify captured variable | Notable rule |
|---|---|---|---|
| JavaScript | By reference to the binding | Yes | let in loops creates a fresh binding per iteration |
| Python | By reference, late binding | Only with nonlocal | Loop variables are read at call time |
| Java | Value of effectively final variables | No | Lambdas reject reassigned locals |
| C++ | Explicit capture list | With reference capture | [=] copies, [&] references |
| Rust | Borrow or move, inferred | With mutable borrow | move transfers ownership into the closure |
| Swift | Strong reference | Yes | Capture lists such as [weak self] break cycles |
Explicit control, as in C++ capture lists and Rust’s move keyword, trades convenience for predictable lifetimes and memory use. Implicit reference capture, as in JavaScript and Python, is concise but produces the late-binding surprises described below.
Closures among related functional concepts
Closure is often used as a synonym for lambda or anonymous function, but the concepts answer different questions. A lambda is a syntax for writing a function without a name; a closure is a runtime value that carries captured state. A named function can be a closure, and a lambda that uses no outer variables captures nothing.
| Concept | Defining property | Relation to closures |
|---|---|---|
| Lambda | Function without a name | Becomes a closure if it uses outer variables |
| Higher-order function | Takes or returns functions | Usually returns closures |
| Pure function | Same input gives same output, no side effects | A closure over immutable data can stay pure |
| Object with methods | State plus behavior | Closures are an equivalent encapsulation tool |
| Partial application | Fixes some arguments in advance | Implemented by capturing those arguments |
The link to purity and immutability is important in functional programming. A closure that captures only immutable values behaves like a pure function with preset parameters. A closure that captures and mutates shared state carries hidden side effects, which makes reasoning and testing harder.
Typical uses of closures
Closures appear wherever behavior must be configured once and executed later. The most common patterns are listed below.
- Callbacks and event handlers that remember the context in which they were registered.
- Function factories such as a
makeMultiplier(3)that returns a tripling function. - Partial application and currying, which fix arguments step by step.
- Data privacy, as in the JavaScript module pattern, where captured variables are unreachable from outside.
- Memoization, where a wrapper captures a cache keyed by arguments.
- Decorators and middleware that wrap a function with logging, timing or authorization.
In React, hooks rely heavily on closures, and stale closures are a well-known class of bug: an effect or callback keeps reading the state value from the render in which it was created. Dependency arrays and functional state updates exist to control exactly this capture.
Pitfalls and how to avoid them
Most closure bugs come from capturing more, or for longer, than intended. The table links common symptoms to their causes and remedies.
| Symptom | Cause | Remedy |
|---|---|---|
| All callbacks print the last loop value | Late binding of a shared loop variable | Use block-scoped variables or bind the value as a parameter |
| Memory grows in a long-running app | Closure keeps a large object graph alive | Capture only needed fields, remove listeners |
| Object never deallocated in Swift | Strong reference cycle through self | Use [weak self] or [unowned self] |
| UI shows outdated state | Stale closure from an earlier render | Update dependencies or read current state |
| Race on shared captured data | Concurrent closures mutate one binding | Capture immutable copies or synchronize access |
Performance overhead is usually small, typically one heap allocation per closure creation, but creating closures in tight loops of hot code can add measurable garbage collection pressure. In such paths, hoisting the closure out of the loop is a simple and effective fix.
In simple terms
A closure is a function that remembers where it came from. Think of a note written in a meeting that still makes sense later because it was clipped to the meeting agenda. Even after the meeting ends, the note keeps its context.
In code, a closure keeps access to the variables that were around when it was created. This memory is what lets a small function carry its own settings or private data without anyone else being able to change them.
A practical example
A front-end developer needs to limit how often a search box sends requests to a server while a user types. She writes a debounce function that takes a function and a delay of 300 milliseconds and returns a new function. The returned function is a closure that captures a private timer variable shared across calls.
Each keystroke calls the closure, which clears the previous timer and starts a new one, so the request fires only after the user pauses. No global variable is needed, and each search box gets its own independent timer because each call to debounce creates a separate environment. Request volume to the search endpoint drops by roughly an order of magnitude while the interface still feels immediate.