Closures are consistently the most asked-about topic in JavaScript interviews. But most explanations focus on the definition — "a function that remembers its outer scope" — without explaining why this happens, or what you can actually do with it.
This guide builds the real mental model, then shows you the interview patterns that follow from it.
The Setup: What Is a Scope?
Before closures make sense, scope needs to be precise. A scope is the set of variables a piece of code can access at any given point. JavaScript uses lexical scoping — a function's scope is determined by where it's written, not where it's called.
const x = 'global'
function outer() { const x = 'outer'
function inner() { const x = 'inner' console.log(x) // 'inner' — finds it locally first }
inner() console.log(x) // 'outer' }
outer() console.log(x) // 'global'
Each function has its own scope. When looking up a variable, JavaScript moves outward through enclosing scopes until it finds it or hits the global scope.
What Actually Creates a Closure
A closure is created whenever a function accesses a variable from an enclosing scope. That's it. There's no special keyword. No opt-in. Every time an inner function references an outer variable, a closure exists.
function outer() {
const message = 'hello' // variable in outer scope
function inner() { console.log(message) // inner accesses outer's variable — closure }
return inner }
const fn = outer() fn() // 'hello' — outer() has returned, but 'message' survives
This is the counterintuitive part: outer() has finished executing. Its call stack frame is gone. But message is still alive in memory — because inner holds a reference to it. The garbage collector can't collect a variable as long as a function referencing it still exists.
The Three-Sentence Definition Interviewers Want to Hear
A closure is a function bundled together with a reference to its surrounding lexical environment. The function retains access to variables from its outer scope even after that outer function has returned. This happens automatically in JavaScript whenever an inner function references variables from an enclosing scope.
Say this, then demonstrate it with code. That's the formula.
Five Patterns Closures Enable
1. Private state (data encapsulation)
JavaScript has no private modifier for standalone functions. Closures give you genuine privacy:
function createBankAccount(initialBalance) {
let balance = initialBalance // private — inaccessible from outside
return { deposit(amount) { if (amount <= 0) throw new Error('Invalid deposit') balance += amount }, withdraw(amount) { if (amount > balance) throw new Error('Insufficient funds') balance -= amount }, getBalance() { return balance } } }
const account = createBankAccount(100) account.deposit(50) account.getBalance() // 150 account.balance // undefined — no direct access
There's no way to read or modify balance except through the returned methods. It's genuinely private.
2. Function factories
Closures power functions that generate specialized functions:
function multiplier(factor) {
return (n) => n * factor // closes over factor
}
const double = multiplier(2) const triple = multiplier(3) const tax = multiplier(1.0875)
double(5) // 10 triple(5) // 15 tax(99.99) // 108.74
Each returned function is independent. Each closes over its own factor.
3. Memoization
Cache expensive computations — the cache lives in the closure:
function memoize(fn) {
const cache = {} // private to this memoize call
return function(...args) { const key = JSON.stringify(args) if (key in cache) { console.log('cache hit') return cache[key] } cache[key] = fn(...args) return cache[key] } }
const expensiveCalc = memoize((n) => { console.log('computing...') return n n n })
expensiveCalc(5) // computing... 125 expensiveCalc(5) // cache hit 125 expensiveCalc(6) // computing... 216
4. Partial application
Pre-supply some arguments, get a specialized function:
function request(method, baseUrl) {
return function(path, data) {
return fetch(${baseUrl}${path}, {
method,
body: JSON.stringify(data)
})
}
}
const post = request('POST', 'https://api.example.com') const get = request('GET', 'https://api.example.com')
post('/users', { name: 'Alice' }) get('/users/1')
5. Event handlers that remember state
function createToggle(element) {
let isOpen = false // state lives in the closure
element.addEventListener('click', () => { isOpen = !isOpen element.setAttribute('aria-expanded', isOpen) element.textContent = isOpen ? 'Close' : 'Open' }) }
Without closures, the click handler would have no way to remember state between clicks.
The Loop Problem — The Most Common Interview Question
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i)
}, i * 1000)
}
// Output: 5, 5, 5, 5, 5 — all after 0s, 1s, 2s, 3s, 4s
Why? var is function-scoped. There's one i in memory. All five closures share it. By the time any callback fires, the loop is done and i is 5.
Fix 1: let — creates a new binding per iteration:
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log(i), i * 1000)
}
// 0, 1, 2, 3, 4 ✓
Fix 2: IIFE — captures the current value by creating a new scope:
for (var i = 0; i < 5; i++) {
;(function(j) {
setTimeout(() => console.log(j), j * 1000)
})(i)
}
// 0, 1, 2, 3, 4 ✓
The IIFE creates a new scope. j is a new variable that receives i's current value. The closure over j captures a different variable per iteration.
Closures and Memory — The Leak You Don't See Coming
Closures keep their referenced variables alive as long as the function exists. This is powerful — and dangerous when the function outlives its usefulness:
// ❌ This holds an entire array in memory for the lifetime of the app
function processResults(data) {
const processed = data.map(heavyTransform) // potentially huge
return function getFirst() { return processed[0] // only needs the first item // but 'processed' (the whole array) stays alive because of this closure } }
// ✓ Extract only what the closure needs function processResults(data) { const processed = data.map(heavyTransform) const first = processed[0] // only keep what we need // 'processed' is now eligible for GC after this function returns return () => first }
The rule: a closure keeps everything it references alive. If it only needs one value, extract that value and close over that — not the entire parent object.
Three Questions Interviewers Will Ask
Q: What is a closure? A function combined with references to the variables in its lexical scope. The function retains access to outer variables even after the outer function returns. Demonstrate with a counter or bank account example.
Q: What's the difference between a closure and a scope? Scope is the set of variables accessible at a given point in code — it exists at the time the code runs. A closure is a function that carries its scope with it — it preserves the binding to variables from its defining scope, keeping them alive after the creating function has returned.
Q: Can closures cause memory leaks? How? Yes — if a closure references a large object and the closure itself is long-lived (attached to a DOM element, a global variable, an event listener), the referenced objects are never garbage collected. Fix by extracting only needed values into variables, and removing closure-holding references when done (unregistering event listeners, clearing timers).
The One-Line Summary
Closures are what allows functions in JavaScript to be first-class values — you can return a function, store it, pass it around, and it brings its environment with it. Without closures, returning a function would be meaningless. With them, it's one of the most expressive tools in the language.