Closures Interview Questions & Answers
34 questions Updated 2026-06-17
JavaScript closure interview questions and answers — what closures are, how they capture variables, practical uses and common pitfalls.
Read the in-depth guideJavaScript Closures Explained — From Basics to Advanced PatternsA 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 and that outer call's scope would otherwise be gone.
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 key insight interviewers want: the closure captures the variable itself
(the binding), not a copy of its value. So count is genuinely shared and
mutable across calls — each next() sees and updates the same count. Every
function in JavaScript is technically a closure; it only matters when the
function outlives the scope it captured.
Closures are everywhere in real JavaScript, usually to remember state between calls without a global variable or a class:
- Data privacy / encapsulation — the module pattern, private counters, anything you don't want callers to touch directly.
- Function factories & currying / partial application — pre-fill some arguments and return a specialized function.
- Callbacks & event handlers — a handler remembers the data it was set up with.
- Memoization — keep a private cache between invocations.
function memoize(fn) {
const cache = new Map() // private, persists across calls
return arg => {
if (cache.has(arg)) return cache.get(arg)
const result = fn(arg)
cache.set(arg, result)
return result
}
}
Yes. A closure keeps its captured scope alive for as long as the closure itself is reachable. If that closure outlives its usefulness while holding references to large objects or detached DOM nodes, the garbage collector can't reclaim them — a leak.
function attach() {
const huge = new Array(1_000_000).fill('🐘')
document.getElementById('btn').addEventListener('click', () => {
// this handler closes over `huge`, pinning it in memory forever
console.log(huge.length)
})
}
Mitigations: remove event listeners when you're done
(removeEventListener), null out references you no longer need, avoid
capturing more than necessary, and consider WeakMap/WeakRef for caches so
entries can be collected when nothing else references the key.
With var, the loop variable is function-scoped, so there's a single
binding shared by every iteration. By the time the deferred callbacks run, the
loop has finished and they all read that one binding's final value. let
is block-scoped and creates a fresh binding per iteration, so each
callback closes over its own copy.
// all share one `i`, which is 3 when the timeouts fire
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i)) // 3 3 3
// a new `i` per iteration
for (let i = 0; i < 3; i++) setTimeout(() => console.log(i)) // 0 1 2
Before let existed, the fix was to create a new scope manually with an IIFE:
(j => setTimeout(() => console.log(j)))(i). This question is really testing
whether you understand binding vs value capture.
Declare variables in an enclosing function scope and return only the functions that should be allowed to touch them. The variables are unreachable from outside — there's no reference to them except through the methods you exposed.
function account() {
let balance = 0 // truly private — no outside access
return {
deposit: n => (balance += n),
withdraw: n => (balance -= n),
get: () => balance,
}
}
const a = account()
a.deposit(100)
a.get() // 100
a.balance // undefined — can't reach the closed-over variable
This was the canonical way to get encapsulation before JavaScript added
#private class fields, and it still underlies the module pattern.
Currying transforms a function that takes many arguments into a chain of functions that each take one. Closures make it work: each returned function remembers the arguments supplied to the earlier ones via its captured scope.
const add = a => b => c => a + b + c
add(1)(2)(3) // 6
// practical: build specialized functions by partially applying
const multiply = a => b => a * b
const double = multiply(2) // remembers a = 2
double(10) // 20
Each inner arrow closes over the parameters of the outer arrows, which is why
double keeps a = 2 ready for whenever you call it. Useful for
configuration, reusable utilities, and point-free function composition.
An Immediately Invoked Function Expression is a function that runs the
instant it's defined — you wrap it in parentheses to make it an expression
and immediately call it with ().
(function () {
const secret = 42 // scoped to here, not global
console.log(secret)
})()
Before block scoping (let/const) and ES modules, the IIFE was the main tool
for creating a private scope: it kept variables out of the global namespace
(avoiding name collisions between scripts) and was the backbone of the module
pattern. Today let/const blocks and real modules cover most of these cases,
so you see IIFEs less often.
They're related but not the same thing:
- Scope is a static concept: the set of variables accessible at a given location in your code, determined by where things are written (lexical scoping). It exists whether or not any function "leaves."
- A closure is what you get at runtime when a function retains access to its defining scope and carries it along to be executed somewhere else — after the outer function has returned.
function outer() {
const x = 10 // x is in outer's SCOPE
return () => x // the returned function + its hold on x = a CLOSURE
}
const fn = outer() // outer's scope is gone, but the closure keeps x alive
fn() // 10
Put simply: scope defines what a function can see; a closure is the function remembering and preserving that view beyond its original lifetime.
The module pattern uses a closure (classically an IIFE) to create private state and expose only a public API — the returned object's methods close over variables that are otherwise inaccessible.
const counter = (function () {
let count = 0 // private
return {
increment() { count++ },
value() { return count },
}
})()
counter.increment()
counter.value() // 1
counter.count // undefined — truly private
It was the standard way to organize code and hide internals before ES modules.
Today import/export modules serve the same purpose at the file level, but the
pattern still appears for encapsulated singletons.
Debounce delays running a function until input stops for a quiet period. A closure holds the pending timer id across calls so each new call can cancel the previous one.
function debounce(fn, delay) {
let timer // remembered between calls via closure
return function (...args) {
clearTimeout(timer)
timer = setTimeout(() => fn.apply(this, args), delay)
}
}
const onSearch = debounce(query => fetchResults(query), 300)
Each keystroke resets the timer; fn runs only 300ms after the last call.
The private timer variable is exactly the kind of persistent state closures
exist for.
Throttle runs the function at most once per interval (regular cadence), whereas debounce waits until activity stops. A closure tracks the last run time.
function throttle(fn, limit) {
let last = 0
return function (...args) {
const now = Date.now()
if (now - last >= limit) {
last = now
fn.apply(this, args)
}
}
}
const onScroll = throttle(() => updatePosition(), 100)
Use throttle for continuous events where you want steady updates (scroll, resize, mousemove); use debounce for "act after they finish" (search input, autosave).
once wraps a function so it runs only the first time and returns the cached
result thereafter. A closure remembers whether it has been called and the result.
function once(fn) {
let called = false, result
return function (...args) {
if (!called) {
called = true
result = fn.apply(this, args)
}
return result
}
}
const init = once(() => console.log('init!'))
init() // 'init!'
init() // (nothing — cached)
Useful for one-time initialization, idempotent event handlers, and lazy singletons.
Memoization caches results of expensive pure functions by their arguments. A closure holds a private cache that persists across calls.
function memoize(fn) {
const cache = new Map() // private, lives between calls
return function (...args) {
const key = JSON.stringify(args)
if (cache.has(key)) return cache.get(key)
const result = fn.apply(this, args)
cache.set(key, result)
return result
}
}
const slowSquare = memoize(n => n * n)
The cache is invisible to callers — pure encapsulation. Only memoize pure
functions, and mind cache growth (a WeakMap or LRU cap helps for large key
spaces).
Partial application fixes some arguments of a function now, returning a new function that takes the rest later. The returned function closes over the pre-supplied arguments.
function partial(fn, ...preset) {
return (...rest) => fn(...preset, ...rest) // closes over `preset`
}
const add = (a, b, c) => a + b + c
const add10 = partial(add, 10)
add10(20, 30) // 60
It differs from currying (which takes args one at a time); partial application fixes any number at once. Both rely on closures to remember the bound arguments.
An event handler defined inside a function closes over that function's variables, so each handler "remembers" the data it was created with — without needing global state or data attributes.
function setupButtons(labels) {
labels.forEach((label, i) => {
button[i].addEventListener('click', () => {
console.log(`clicked ${label} (#${i})`) // closes over label and i
})
})
}
Each handler captures its own label/i. (Using let/forEach gives a fresh
binding per iteration — the same reason the classic var loop bug doesn't
appear here.)
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100)
}
// 3 3 3
All three arrow functions close over the same var i (function-scoped). By
the time the timers fire (after the loop completes), i is 3. Fixes:
for (let i = 0; i < 3; i++) setTimeout(() => console.log(i), 100) // 0 1 2
// or capture per iteration with an IIFE:
for (var i = 0; i < 3; i++) ((j) => setTimeout(() => console.log(j)))(i)
The lesson: closures capture the variable binding, not a snapshot of its value.
Closures capture the variable (binding), not a copy of its value. So if the variable changes after the closure is created, the closure sees the new value.
let x = 1
const read = () => x
x = 99
read() // 99 — not 1
This is why the var loop bug happens (all closures share one binding) and why
let fixes it (a fresh binding each iteration). To capture a value snapshot,
copy it into a new scope (an IIFE parameter or a let in the loop body).
Lexical (static) scoping means a function's accessible variables are determined by where it is written in the source, not where it's called. Closures are the runtime consequence: a function carries its lexical environment with it.
function outer() {
const secret = 42
function inner() { return secret } // resolves `secret` by lexical position
return inner
}
outer()() // 42 — inner remembers outer's scope wherever it runs
Because scope is fixed at definition time, you can reason about what a closure can access just by reading the nested structure of the code.
Yes. Every invocation of an outer function creates a fresh scope, so any inner functions returned capture independent copies of that scope. Two calls to a factory produce two unrelated states.
function counter() { let n = 0; return () => ++n }
const a = counter()
const b = counter()
a() // 1
a() // 2
b() // 1 — separate closure, separate `n`
This independence is what makes closure-based factories useful: each produced object has its own private state.
A function factory is a function that returns customized functions, each closing over the arguments used to create it. It's a clean way to generate specialized variants.
function makeMultiplier(factor) {
return n => n * factor // closes over `factor`
}
const double = makeMultiplier(2)
const triple = makeMultiplier(3)
double(5) // 10
triple(5) // 15
Each returned function remembers its own factor. Factories built on closures
power currying, partial application, and configurable utilities.
No — a closure captures variables from its lexical scope, but this is not
a normal 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 (this is not obj)
obj.arrow()() // 'Ada' (arrow closes over obj's this)
A common pre-arrow workaround was const self = this, which does capture this
as an ordinary closed-over variable.
Both can hide state. Closures give true privacy via captured variables, with
a functional feel and no this. Classes are more familiar, support
inheritance and (now) #private fields, and are more memory-efficient when you
create many instances (methods live once on the prototype).
// closure
const make = () => { let n = 0; return { inc: () => ++n } }
// class
class C { #n = 0; inc() { return ++this.#n } }
Closures excel for small factories and one-offs; classes scale better for many
instances and rich hierarchies. With #private fields, classes now match
closures on genuine privacy.
A variation of the module pattern where you define all functions/variables privately and return an object that maps public names to those private members — making the public API explicit at the bottom.
const calc = (function () {
let total = 0
function add(n) { total += n }
function get() { return total }
return { add, get } // "reveal" only these
})()
It improves readability (the exposed surface is listed in one place) and lets you rename the public API independently of the private implementation.
Both keep state between calls. A closure holds state in captured variables a
returned function reads/writes. A generator pauses at yield and resumes,
preserving its entire execution state — better for sequences and lazy
iteration.
// closure counter
const counter = (() => { let n = 0; return () => ++n })()
// generator counter
function* gen() { let n = 0; while (true) yield ++n }
const g = gen()
g.next().value // 1
Use a closure for simple persistent state; a generator when you need to produce a stream of values or model a resumable process.
A callback closes over the variables from the moment it was created. If those values change later but the callback isn't recreated, it keeps using the old ("stale") snapshot.
let count = 0
const timer = setInterval(() => console.log(count), 1000) // always logs 0
count = 5 // the closure captured the binding, but in React each render has its own
In React, each render creates new closures over that render's props/state, so an
effect or event handler with stale deps logs old values. Fixes: include the value
in dependencies, use a functional updater, or read the latest from a ref.
A closure keeps its captured variables alive as long as the closure itself is reachable. Once nothing references the closure, the captured scope becomes eligible for garbage collection.
function make() {
const big = new Array(1e6)
return () => big.length
}
let fn = make() // `big` is retained because fn closes over it
fn = null // now `big` can be collected
Engines optimize by capturing only the variables actually used, but holding onto closures (e.g. in a long-lived array or an un-removed event listener) indefinitely retains their scope — a common leak source.
An inner function can close over variables from multiple enclosing scopes at once, walking up the scope chain. Each level's variables remain accessible.
function a(x) {
return function b(y) {
return function c(z) {
return x + y + z // closes over x (from a) AND y (from b)
}
}
}
a(1)(2)(3) // 6
This layered capture is exactly how currying works. The innermost function holds references to every outer scope it uses, keeping them all alive.
Callbacks passed to map/filter/reduce close over variables in their
surrounding scope, letting them reference outside data without extra parameters.
function aboveThreshold(numbers, threshold) {
return numbers.filter(n => n > threshold) // closes over `threshold`
}
aboveThreshold([1, 5, 10], 4) // [5, 10]
The arrow remembers threshold from the enclosing function. This is closures at
work in everyday code, usually without anyone calling it "a closure."
Variables captured by a closure remain valid after awaits and across async gaps — the closure keeps the scope alive. But beware capturing a value that changes during the wait.
async function process(items) {
for (const item of items) {
await save(item)
// `item` is correctly captured per iteration (let/const)
}
}
let/const loop variables give each iteration a fresh binding, so async
callbacks see the right value. With var, the shared binding would be stale by
the time the awaited work resumes.
Return an object whose methods all close over the same private variable, so they share controlled access to it while the outside world can't touch it directly.
function createCounter(start = 0) {
let count = start
return {
increment: () => ++count,
decrement: () => --count,
reset: () => (count = start),
value: () => count,
}
}
const c = createCounter(10)
c.increment(); c.value() // 11
All four methods operate on one private count. This is the closure-based
alternative to a class with a private field.
An IIFE creates a new scope per iteration, capturing the current value as a parameter so each callback closes over its own copy.
for (var i = 0; i < 3; i++) {
(function (j) {
setTimeout(() => console.log(j), 100) // j is this iteration's copy
})(i)
}
// 0 1 2
The IIFE is immediately invoked with i, binding it to the local j. This was
the standard pre-ES6 fix; today simply using let (which block-scopes per
iteration) is cleaner.
Each closure retains its captured variables in memory for as long as it lives, so creating many closures (e.g. a new function per array element in a hot loop) uses more memory than sharing one function. Modern engines optimize aggressively, capturing only used variables.
// creates N closures, each retaining scope
items.map(item => () => process(item))
In practice closures are cheap and the readability/encapsulation wins outweigh the cost — but in performance-critical code with huge numbers of closures, reusing functions or avoiding unnecessary capture can matter.
An IIFE runs once and returns a single shared instance; the closure caches it so every access returns the same object.
const config = (function () {
let instance
function create() { return { loadedAt: Date.now() } }
return {
get() {
if (!instance) instance = create() // created once
return instance
},
}
})()
config.get() === config.get() // true — same instance
The private instance variable, hidden in the closure, guarantees a single
shared object — the closure-based singleton pattern.
Practice tests are coming soon
Get notified when interactive mock interviews and quizzes launch.