JavaScript closures, explained
Closures are one of the most powerful and most misunderstood features of JavaScript.
They're behind data privacy, function factories, currying, memoization, debounce and
throttle, and the famous var-in-a-loop bug. Once the underlying idea clicks, a whole
class of "why does this happen?" questions answers itself. This guide goes from the
core definition to the practical patterns built on it.
What a closure is
A closure is a function bundled together with a reference to the lexical scope in which it was created. Because of that bundle, the inner function keeps access to its outer variables even after the outer function has returned.
function counter() {
let count = 0 // lives in counter's scope
return () => ++count // this arrow closes over count
}
const next = counter() // counter() has returned...
next() // 1 // ...yet count is still alive and remembered
next() // 2
The crucial detail: a closure captures the variable (binding), not a snapshot of its
value. So count is genuinely shared and mutable across calls. Technically every
function is a closure; it only matters when the function outlives the scope it
captured.
Lexical scope and the scope chain
"Lexical" means scope is determined by where code is written, not where it's called. When you reference a variable, the engine searches the current scope, then each enclosing scope outward, until the global scope — the scope chain.
function outer() {
const x = 10
function inner() { return x } // resolves x by lexical position
return inner
}
outer()() // 10 — inner remembers outer's scope wherever it runs
Because scope is fixed at definition time, you can reason about what any closure can access just by reading the nesting. And because each call to an outer function creates a fresh scope, each returned closure captures independent state:
const a = counter()
const b = counter()
a(); a() // 2
b() // 1 — separate closure, separate count
Value vs binding: the loop bug
The most famous closure question. With var (function-scoped), every iteration shares
one binding, so deferred callbacks all read its final value. let (block-scoped)
creates a fresh binding per iteration.
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i)) // 3 3 3
for (let i = 0; i < 3; i++) setTimeout(() => console.log(i)) // 0 1 2
Before let, the fix was an IIFE that manufactured a new scope per iteration, capturing
the value as a parameter:
for (var i = 0; i < 3; i++) ((j) => setTimeout(() => console.log(j)))(i) // 0 1 2
This is the same mechanism behind stale closures in callbacks and React renders: a function captures the values from the render/iteration it was created in, and never sees later changes unless it's recreated.
Data privacy and the module pattern
Variables in an enclosing scope are unreachable from outside — there's no reference to them except through the functions you expose. That's true encapsulation.
function account() {
let balance = 0 // private
return {
deposit: n => (balance += n),
withdraw: n => (balance -= n),
get: () => balance,
}
}
const a = account()
a.deposit(100)
a.get() // 100
a.balance // undefined — inaccessible
The module pattern (often an IIFE) uses this to keep private state and expose only a public API — the standard way to organize code before ES modules, and still useful for encapsulated singletons.
Function factories, currying, partial application
A function factory returns customized functions, each closing over its creation arguments:
const makeMultiplier = factor => n => n * factor
const double = makeMultiplier(2) // remembers factor = 2
double(5) // 10
Currying transforms a multi-argument function into a chain of single-argument ones, each closing over earlier args; partial application fixes some arguments now and returns a function for the rest. Both rely entirely on closures remembering the bound values.
const add = a => b => c => a + b + c
add(1)(2)(3) // 6
Stateful utilities: memoize, once, debounce, throttle
Closures shine when you need persistent private state between calls. A memoizer keeps a private cache:
function memoize(fn) {
const cache = new Map() // private, persists across calls
return arg => cache.has(arg) ? cache.get(arg) : cache.set(arg, fn(arg)).get(arg)
}
Debounce and throttle hold a timer/timestamp in the closure. Debounce runs after activity stops; throttle runs at most once per interval:
function debounce(fn, delay) {
let timer // remembered between calls
return (...args) => {
clearTimeout(timer)
timer = setTimeout(() => fn(...args), delay)
}
}
A once wrapper remembers whether it has run; an encapsulated counter exposes
increment/reset methods over a private variable. All the same idea: state hidden in
a closure.
Closures and this
A subtle point: closures capture variables, but this is not an ordinary
variable for regular functions — it's set by how the function is called. Arrow
functions, however, capture this lexically, like a closed-over variable.
const obj = {
name: 'Ada',
regular() { return function () { return this.name } }, // this lost
arrow() { return () => this.name }, // this captured
}
obj.regular()() // undefined
obj.arrow()() // 'Ada'
The pre-arrow workaround const self = this captured this as a real closed-over
variable.
Memory considerations
A closure keeps its captured scope alive as long as the closure is reachable. That's usually fine, but holding a closure that references large objects or DOM nodes — for example an event listener you never remove — retains that memory indefinitely.
const big = new Array(1e6)
el.addEventListener('click', () => console.log(big.length)) // pins `big`
Release references when done (remove listeners, null large objects), and consider
WeakMap/WeakRef for caches so entries can be collected.
Recap
A closure is a function plus the lexical scope it remembers, captured by binding, not value — which explains the loop bug, stale closures, and shared vs independent state. That single mechanism powers data privacy (module pattern, private variables), function factories, currying and partial application, and stateful utilities like memoize, debounce, and throttle. Understand binding capture and lexical scope, mind the memory implications, and closures become a tool you reach for deliberately rather than a source of mystery.