Closures — Functions That Remember
IntermediateA closure is a function together with the scope it was created in. It keeps that scope alive after the outer function has returned, which is how JavaScript does private state, factories, and almost every callback pattern.
Overview
Closures are the concept interviewers use to separate people who have written JavaScript from people who have read about it. The mechanism is simple: when a function is created, it captures a reference to the scope around it, and that scope survives as long as the function does. What makes it worth understanding is how much of the language rests on it — module privacy, React hooks, event handlers holding onto props, once-only initialisation, memoisation. If you have used any of those, you have used closures whether you named them or not.
The Mechanism
The inner function keeps a live reference to the outer scope — not a copy. Each call to the outer function creates a new, independent scope.
function counter() {
let count = 0 // lives on after counter() returns
return {
increment: () => ++count,
value: () => count,
}
}
const a = counter()
const b = counter() // independent scope
a.increment(); a.increment()
a.value() // 2
b.value() // 0
// count is unreachable from outside — genuine private state,
// which JavaScript had no other way to express until #private fields.It Captures the Variable, Not the Value
This is the detail that causes bugs. The closure sees whatever the variable holds when the function runs, not when it was created.
// Captures the binding — and var has one binding for the whole loop
const fns = []
for (var i = 0; i < 3; i++) fns.push(() => i)
fns.map(f => f()) // [3, 3, 3]
// let gives each iteration its own binding
const fns2 = []
for (let i = 0; i < 3; i++) fns2.push(() => i)
fns2.map(f => f()) // [0, 1, 2]
// In React, this is the "stale closure" problem:
useEffect(() => {
const id = setInterval(() => console.log(count), 1000)
return () => clearInterval(id)
}, []) // count is captured once and never updates
// Fix: add count to deps, or use the updater form of setStateWhat Closures Are Actually Used For
Beyond interview questions, these are the patterns you will write.
// Memoisation — the cache lives in the closure
function memoise(fn) {
const cache = new Map()
return (...args) => {
const key = JSON.stringify(args)
if (!cache.has(key)) cache.set(key, fn(...args))
return cache.get(key)
}
}
// Debounce — the timer id lives in the closure
function debounce(fn, ms) {
let timer
return (...args) => {
clearTimeout(timer)
timer = setTimeout(() => fn(...args), ms)
}
}
const search = debounce(q => fetchResults(q), 300)Key Points to Remember
- 1A closure is a function plus the scope it was defined in, kept alive after the outer function returns
- 2Each call to the outer function creates an independent scope — two counters do not share state
- 3Closures capture the variable, not its value at creation time; this is what makes stale closures possible
- 4Private state, memoisation, debounce, throttle and React hooks are all closures underneath
- 5The classic var-in-a-loop bug is a closure capturing one shared binding instead of three
Interview Questions
Sign in to ask AriaWhat is a closure? Give an example where you would use one.
Implement a debounce function.
What is a stale closure, and how does it appear in a React useEffect?
Ask Aria about Closures — Functions That Remember
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.