Closures are one of those concepts that are easy to use without understanding and easy to misunderstand once you try to explain them. Every React useEffect callback, every Node.js event handler, and every Python decorator relies on closures. Understanding the mechanics prevents the bugs — especially the infamous loop-variable capture bug that trips up developers at every experience level.
What changed in 2026
- React 19 improved closure semantics in hooks. The new
use hook and compiler-optimised useMemo reduce stale-closure bugs that plagued useEffect in React 16–18.
- Python 3.13 improved
__closure__ inspection. The new inspect.getclosurevars API (stabilised in 3.13) makes closure debugging far more accessible.
- Rust closures are now explained in the stdlib docs with lifetime examples. The ownership model makes closure capture semantics explicit in ways that help developers from other languages understand what is really happening.
- LLM-generated callbacks often contain stale closure bugs. AI assistants generate
useEffect and setTimeout callbacks that capture stale values. Recognising the pattern is a critical skill.
What is a closure?
A closure is a function that captures variables from its lexical scope — the scope where it was defined, not where it is called.
def make_counter(start=0):
count = start # this variable is "closed over"
def increment():
nonlocal count
count += 1
return count
return increment
counter = make_counter(10)
print(counter()) # 11
print(counter()) # 12
# `count` lives on even though `make_counter` has returned
increment closes over count. The variable continues to exist in memory as long as increment exists, even though make_counter's stack frame is gone.
Closures in JavaScript
JavaScript closures are everywhere — event handlers, callbacks, module patterns, React hooks.
function makeAdder(x) {
return function(y) {
return x + y; // closes over `x`
};
}
const add5 = makeAdder(5);
console.log(add5(3)); // 8
console.log(add5(7)); // 12
The inner function retains a reference to x from makeAdder's scope. add5 and add10 each have their own independent x because each call to makeAdder creates a new scope.
The loop closure bug
The most famous closure pitfall: capturing a loop variable by reference.
// Bug: all callbacks see `i = 5` (the final value)
const funcs = [];
for (var i = 0; i < 5; i++) {
funcs.push(function() { return i; });
}
funcs.map(f => f()); // [5, 5, 5, 5, 5]
// Fix 1: use `let` — block-scoped, new binding each iteration
for (let i = 0; i < 5; i++) {
funcs.push(function() { return i; });
}
funcs.map(f => f()); // [0, 1, 2, 3, 4]
// Fix 2: IIFE to capture the value immediately
for (var i = 0; i < 5; i++) {
funcs.push((function(j) { return function() { return j; }; })(i));
}
The same bug appears in Python with mutable default arguments or late-binding closures:
# Bug: all lambdas capture the same `i` variable
funcs = [lambda: i for i in range(5)]
[f() for f in funcs] # [4, 4, 4, 4, 4]
# Fix: capture by value with a default argument
funcs = [lambda i=i: i for i in range(5)]
[f() for f in funcs] # [0, 1, 2, 3, 4]
Closures in Rust
Rust closures capture by move (move keyword) or by borrow, with lifetime guarantees enforced at compile time.
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y // `move` copies `x` into the closure
}
let add5 = make_adder(5);
println!("{}", add5(3)); // 8
move is required here because x is a stack variable; without move, the closure would hold a reference to a local that no longer exists.
Closures and memory
A closure keeps every captured variable alive for its own lifetime. Capturing a large object unnecessarily extends its lifetime.
function processData(largeBuffer) {
const summary = summarise(largeBuffer);
// Bug: `largeBuffer` stays in memory because the closure captures it
return function() { return summary; };
// Fix: don't capture `largeBuffer` — only capture `summary`
// The above already does this correctly if `largeBuffer` is
// not referenced inside the inner function.
}
In React, stale closures in useEffect are the most common manifestation: the callback captures an old value of a state variable because the effect dependency array is wrong.
Comparison table: closure semantics by language
| Language |
Capture mechanism |
Mutable captures |
Stale value risk |
| JavaScript |
By reference (variables) |
Yes (let/var) |
High — useEffect, setTimeout |
| Python |
By reference (cells) |
Yes with nonlocal |
Medium — loop variable |
| Rust |
Move or borrow (explicit) |
Yes with FnMut |
Low — compiler enforces |
| Swift |
By reference (default) |
Yes with @escaping |
Medium |
| Go |
By reference |
Yes |
Medium — goroutine loop bug |
How to pick (use cases for closures)
- Callback / event handler? → Closure is the natural fit; be explicit about what you capture.
- Factory function that returns specialised functions? → Closure over configuration variables.
- Partial application? → Closure or
functools.partial in Python.
- Complex mutable state shared across many methods? → Class, not a closure.
- Goroutine or async task capturing a loop variable? → Copy the variable before the goroutine/closure.
Common mistakes
Not using nonlocal in Python. Assigning to a captured variable without nonlocal creates a new local variable, shadowing the outer one — a silent bug.
Stale closure in React useEffect. The effect captures a state value at the time of render. If state updates, the closure sees the old value. Fix: add the variable to the dependency array or use useRef.
Capturing this in a class method. Arrow functions inherit this from the enclosing scope; regular functions do not. This is a closure behaviour difference that trips up JavaScript developers.
Circular reference via closure. An object's method closes over the object; the object holds the method — neither can be GC'd. Use weak references where needed.
What to skip
- Closure-based module pattern in modern JS — ES modules with
import/export are clearer and tree-shakeable; the IIFE module pattern is a 2010-era workaround.
- Deep closure chains — closures returning closures returning closures become hard to follow. Flatten the logic or use a class.
- Closing over mutable DOM elements in long-lived callbacks — it prevents GC of the DOM element even after removal. See Promises explained in 2026 for async closure patterns.
FAQ
Is every anonymous function a closure?
Not necessarily — a function that references no outer variables is technically not a closure (it has nothing to close over). In practice, anonymous functions in most languages are implemented as closures regardless.
Do closures in JavaScript cause memory leaks?
They can, if the closure is held in memory (e.g., as an event listener) longer than needed and it captures a large object. Remove event listeners when done and avoid capturing unnecessary references.
What is the difference between a closure and a lambda?
Lambda is a syntax for anonymous functions. A closure is a semantic property — a function that captures its environment. Most lambdas are closures, but not all closures are written as lambdas.
How do closures interact with garbage collection?
The GC keeps any variable alive as long as a closure that references it is alive. This extends object lifetimes beyond their lexical scope, which is both the power and the memory-leak risk of closures.
Where to go next