Closures are the most frequently tested JavaScript concept. Master them with real interview questions, answers, and code examples.
Every function in JavaScript carries an invisible backpack. The moment a function is created, JavaScript stuffs that backpack with every variable the function can see from the scope where it was written. The function takes that backpack everywhere — passed as a callback, returned from another function, stored in a variable. Even after the outer function has long finished and its stack frame is gone, the inner function's backpack still holds those variables alive in memory. That backpack is a closure. The precise technical definition: a closure is a function bundled together with a reference to its surrounding Lexical Environment. Not a copy of the values — a reference to the actual variable bindings. This means if the outer scope changes a variable, the closure sees the new value. If the closure changes a variable, the outer scope sees it too. They share the same bindings. The most important thing to understand about closures: they capture the variable, not the value at the time of capture.
Every function object has an internal property called [[Environment]] (visible in Chrome DevTools as [[Scopes]]). When a function is created, its [[Environment]] is set to the current Lexical Environment — the Environment Record of the scope where it was written.
When that function is called later, the engine creates a new Lexical Environment for the call, and sets its outer reference to the function's stored [[Environment]]. This is how the function "remembers" its birthplace — through a chain of Environment Records connected by outer references.
function outer() {
let count = 0 // lives in outer's Environment Record
function inner() { // inner's [[Environment]] = outer's Lexical Environment
count++ // looks up count: not in inner → follows outer reference → found
return count
}
return inner
}
const increment = outer() // outer's call stack frame is gone
increment() // 1 // but inner still holds [[Environment]] pointing to outer's record
increment() // 2 // count is ALIVE — kept alive by the closure reference
increment() // 3
When outer() returns, JavaScript would normally garbage collect its Environment Record. But because inner's [[Environment]] slot still references it, the garbage collector sees it as reachable and keeps it in heap memory. This is how closures keep variables alive after their enclosing function returns.
function make() {
let x = 1
const read = () => x // closes over the BINDING of x
const write = (v) => { x = v } // closes over the same BINDING
return { read, write }
}
const { read, write } = make()
console.log(read()) // 1
write(42)
console.log(read()) // 42 — read() sees the mutation write() made
read and write share the same x variable. They don't each have a private copy — they both point into the same Environment Record slot. This is why closures are used for shared mutable state, and why it's also a common bug source when developers expect independence.
The classic loop trap is caused by exactly this:
const fns = []
for (var i = 0; i < 3; i++) {
fns.push(() => i) // all 3 functions close over the SAME 'i' binding
}
console.log(fns[0]()) // 3 ← not 0
console.log(fns[1]()) // 3 ← not 1
console.log(fns[2]()) // 3 ← not 2
// All three closures see i = 3 because they share one binding, read after the loop ends
The fix with let works because let creates a new binding per iteration — each closure gets its own Environment Record with its own i:
const fns = []
for (let i = 0; i < 3; i++) {
fns.push(() => i) // each closure captures a DIFFERENT 'i' binding
}
console.log(fns[0]()) // 0 ✓
console.log(fns[1]()) // 1 ✓
console.log(fns[2]()) // 2 ✓
function createCounter(initial = 0) {
let count = initial // private — no external access
return {
increment: () => ++count,
decrement: () => --count,
reset: () => { count = initial },
value: () => count,
}
}
const c = createCounter(10)
c.increment() // 11
c.increment() // 12
c.decrement() // 11
c.reset()
c.value() // 10 — initial preserved separately in closure
console.log(count) // ReferenceError — count is truly private
function multiplier(factor) {
return (n) => n * factor // factor is captured in [[Environment]]
}
const double = multiplier(2)
const triple = multiplier(3)
const times10 = multiplier(10)
console.log(double(5)) // 10
console.log(triple(5)) // 15
console.log(times10(5)) // 50
// Each returned function has its own Environment Record with its own 'factor'
// double, triple, times10 are independent — they don't share state
function partial(fn, ...fixedArgs) {
return function(...laterArgs) {
return fn(...fixedArgs, ...laterArgs) // fixedArgs captured in closure
}
}
const add = (a, b, c) => a + b + c
const add5 = partial(add, 5) // fixes first arg to 5
const add5and3 = partial(add, 5, 3) // fixes first two args
console.log(add5(10, 20)) // 35 (5 + 10 + 20)
console.log(add5and3(7)) // 15 (5 + 3 + 7)
function memoize(fn) {
const cache = new Map() // captured in closure — persists across calls
return function(...args) {
const key = JSON.stringify(args)
if (cache.has(key)) {
console.log('cache hit')
return cache.get(key)
}
const result = fn.apply(this, args)
cache.set(key, result)
return result
}
}
const expensiveCalc = memoize((n) => {
// imagine this is slow
return n * n
})
expensiveCalc(10) // computed: 100
expensiveCalc(10) // cache hit: 100
expensiveCalc(20) // computed: 400
The cache Map lives in the closure's Environment Record. It persists as long as the memoized function exists. Each memoized function gets its own independent cache — because memoize() creates a new scope each time it's called.
// once: function that only fires the first time
function once(fn) {
let called = false
let result
return function(...args) {
if (called) return result
called = true
result = fn.apply(this, args)
return result
}
}
const init = once(() => { console.log('initialised'); return 42 })
init() // 'initialised', returns 42
init() // silent, returns 42 (cached)
// debounce: delay execution until silence
function debounce(fn, delay) {
let timerId = null // captured in closure, shared across all calls
return function(...args) {
clearTimeout(timerId)
timerId = setTimeout(() => fn.apply(this, args), delay)
}
}
// throttle: fire at most once per interval
function throttle(fn, interval) {
let lastFired = 0 // captured in closure
return function(...args) {
const now = Date.now()
if (now - lastFired >= interval) {
lastFired = now
return fn.apply(this, args)
}
}
}
const CartModule = (function() {
// Private state — nothing outside can access these
let items = []
let discount = 0
// Private function
function calculateTotal() {
const raw = items.reduce((sum, item) => sum + item.price, 0)
return raw * (1 - discount)
}
// Public API — what we choose to expose
return {
addItem(item) { items.push(item) },
removeItem(id) { items = items.filter(i => i.id !== id) },
setDiscount(pct) { discount = pct / 100 },
getTotal() { return calculateTotal() },
getCount() { return items.length },
}
})()
CartModule.addItem({ id: 1, price: 100 })
CartModule.addItem({ id: 2, price: 200 })
CartModule.setDiscount(10)
CartModule.getTotal() // 270
console.log(CartModule.items) // undefined — truly private
This is the pattern used in pre-module JavaScript for encapsulation. ES modules largely replaced it, but it appears in legacy codebases at every major company.
This is asked at Razorpay, Flipkart, Atlassian, and almost every React-heavy interview:
import { useState, useEffect } from 'react'
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
const id = setInterval(() => {
console.log(count) // STALE — always logs 0
setCount(count + 1) // STALE — always sets to 1
}, 1000)
return () => clearInterval(id)
}, []) // empty dependency array — effect runs once
// the callback closes over count = 0 at mount time
// count never updates in this closure
return {count}
}
The closure captures count = 0 from the first render. The interval callback keeps reading that stale binding forever, because the effect never re-runs (empty deps array).
Fix 1 — functional update (preferred):
setCount(prev => prev + 1) // React provides current value — no closure needed
Fix 2 — add count to dependencies:
useEffect(() => {
const id = setInterval(() => setCount(count + 1), 1000)
return () => clearInterval(id)
}, [count]) // re-runs every time count changes — fresh closure each time
Fix 3 — useRef to break the closure:
const countRef = useRef(count)
useEffect(() => { countRef.current = count }, [count])
useEffect(() => {
const id = setInterval(() => {
setCount(countRef.current + 1) // ref always has latest value — no stale closure
}, 1000)
return () => clearInterval(id)
}, []) // safe — reading ref, not the closed-over count
Closures keep their entire captured Environment Record alive. If that record contains large objects or DOM nodes, they cannot be garbage collected as long as the closure exists.
// Memory leak — detached DOM node pattern
function setup() {
const bigElement = document.getElementById('huge-table') // large DOM node
document.addEventListener('click', function handler() {
// handler closes over bigElement — bigElement is captured in [[Environment]]
console.log(bigElement.id)
})
// bigElement is removed from DOM but NOT garbage collected
// because handler's closure keeps a reference to it
bigElement.remove()
// Memory leak: handler + bigElement live forever in the event listener
}
// Fix: explicitly null the reference when done
function setupFixed() {
let bigElement = document.getElementById('huge-table')
function handler() {
console.log(bigElement?.id)
}
document.addEventListener('click', handler)
bigElement.remove()
bigElement = null // bigElement slot in Environment Record now null → GC can collect the node
// OR: remove the listener when no longer needed
// document.removeEventListener('click', handler)
}
V8's optimiser can sometimes detect that certain captured variables are never read and exclude them from the closure's Environment Record — but this is not guaranteed. Always explicitly release large references when closures outlive the data they capture.
| Aspect | Closure | Class |
|---|---|---|
| Privacy | True private — inaccessible outside by language design | Private fields (#) require ES2022+; prototype methods are public by default |
| Memory | Each instance allocates a new Environment Record + function objects | Methods on prototype — shared across all instances, lower per-instance cost |
| Inheritance | Manual composition — explicit function calls | Built-in prototype chain and extends syntax |
| Performance | Slower for many instances (many function objects) | Faster for many instances (shared prototype methods) |
| Readability | Simple for 1-3 methods; complex for larger APIs | Better for rich APIs with many methods |
| Use when | Small, focused utilities; React hooks; functional style | Complex domain objects; OOP hierarchies; large APIs |
// Closure — 3 instances create 9 function objects (3 × 3 methods)
const c1 = createCounter()
const c2 = createCounter()
const c3 = createCounter()
// Class — 3 instances share prototype, only 3 function objects total
class Counter {
#count = 0
increment() { return ++this.#count }
decrement() { return --this.#count }
value() { return this.#count }
}
const cc1 = new Counter(), cc2 = new Counter(), cc3 = new Counter()
const timer = {
message: 'tick',
start() {
setTimeout(function() {
console.log(this.message) // undefined — 'this' is window/undefined in strict mode
// regular functions: 'this' is determined at call time
// setTimeout calls fn with this = global/undefined
}, 1000)
}
}
timer.start() // undefined — not 'tick'
// Fix 1: arrow function (inherits 'this' from enclosing scope lexically)
start() {
setTimeout(() => {
console.log(this.message) // 'tick' — arrow captures 'this' from start()'s context
}, 1000)
}
// Fix 2: close over 'this' explicitly
start() {
const self = this // capture 'this' in a closure variable
setTimeout(function() {
console.log(self.message) // 'tick' — self is captured, not 'this'
}, 1000)
}
Arrow functions don't have their own this — they close over the this of the enclosing regular function's execution context. This is the most common reason to use arrow functions as callbacks.
In Chrome DevTools, setting a breakpoint inside a function shows the Scope panel. Each Closure entry represents one level of the scope chain. The [[Scopes]] internal property of a function object shows the chain of Lexical Environments captured at the moment the function was created.
function outer(x) {
function middle(y) {
function inner(z) {
debugger // DevTools Scope panel shows:
// Local: { z: ... }
// Closure (middle): { y: ... }
// Closure (outer): { x: ... }
// Global: { window, ... }
}
return inner
}
return middle
}
outer(1)(2)(3)
If asked "how would you inspect a closure in the browser?" — breakpoint inside the function, check the Scope panel, each Closure entry is one captured Environment Record.
Every time a function is created, JavaScript allocates:
In hot paths (called millions of times), creating closures in a loop can cause GC pressure:
// GC-heavy — creates a new closure on every render/call
function Component({ items }) {
return items.map(item => ( // new arrow function per item per render
<div onClick={() => select(item.id)}>{item.name}</div>
))
}
// More GC-friendly — stable function reference
function Component({ items }) {
const handleClick = useCallback((id) => select(id), [])
return items.map(item => (
<div onClick={() => handleClick(item.id)}>{item.name}</div>
))
}
V8 optimises closures aggressively — in practice, closure overhead is negligible except in extreme hot paths. But knowing this shows senior-level understanding.
Many developers think closures capture the VALUE of a variable at the time of creation — but closures capture the BINDING. If the variable changes after the closure is created, the closure sees the new value. If the closure changes the variable, outer code sees the mutation. They share a reference, not a snapshot.
Many developers think closures are a special syntax or feature you opt into — but every function in JavaScript is automatically a closure. The moment you write a function, JavaScript attaches its [[Environment]] slot to the current Lexical Environment. There is no special syntax. All functions close over their birthplace scope.
Many developers think the var-in-loop bug happens because closures "copy" the value too late — but the real mechanism is that all closures share the SAME var binding. There's only one 'i' variable for the entire loop with var. By the time callbacks execute, that single 'i' is already 3. let creates a new binding per iteration, giving each closure its own independent 'i'.
Many developers think closures cause inevitable memory leaks — but closures only leak memory if they're never released AND they've captured large objects. A normal closure that gets garbage collected (because nothing else references the function) takes its captured variables with it. The leak pattern requires the closure to outlive the data it captured.
Many developers think closures are only useful for private state — but closures power memoization, partial application, currying, once(), debounce(), throttle(), module patterns, React hooks, and async state management. The counter example is just the simplest introduction.
Many developers think stale closures in React are a React bug — but they're a consequence of closures capturing bindings at function creation time. When a React effect's callback closes over state, it captures the state value at the time the effect function was created (first render, if deps are empty). The "staleness" is correct lexical scoping behaviour — React didn't cause it.
Many developers think each instance of a closure-based object gets its own copy of the captured data — and this is actually true and is precisely why closure-based objects use more memory than class-based objects. Each call to the factory function creates a new Environment Record. Unlike classes where prototype methods are shared, closure functions are separate objects per instance.
React hooks are entirely built on closures. useState returns a setter that closes over the internal fiber's state cell. useEffect callbacks close over props and state at render time. useCallback and useMemo memoize functions and values by closing over their dependencies. Understanding that hooks are closures is the key to understanding stale closure bugs, dependency arrays, and re-render behaviour.
Node.js request handlers close over request context. Express middleware like (req, res, next) => {} creates a closure over the specific request and response objects for that HTTP connection. Database query callbacks in handlers close over req and res, keeping them alive until the async operation completes and the response is sent.
Redux middleware (like redux-thunk and redux-saga) uses closures to give action creators access to dispatch and getState without passing them explicitly. The thunk middleware injects dispatch into a closure: the action creator returns a function that closes over any arguments, and when called receives dispatch — combining closures with dependency injection.
Lodash's _.memoize, _.debounce, and _.throttle are all closure implementations. The entire Lodash utility library is built on the principle that returning a new function from a factory function creates an independent closed-over state per instance. This is why you can have multiple independent debounced versions of the same function.
Browser event listeners naturally create closures over the variables in scope when addEventListener is called. This is why event-based architectures in the browser can hold large amounts of state in memory — every listener potentially keeps its entire captured scope alive until removeEventListener is called.
The module bundler pattern (CommonJS in Node.js) wraps every module in a function: (function(exports, require, module, __filename, __dirname) { yourCode }). This function-as-module creates a closure that gives your code private module-level scope while injecting the module API as parameters. The entire Node.js module system is a closure.
Classic var in loop with setTimeout
var in loop — all closures share one variable
let in loop — each iteration gets own binding
Counter factory shares global state
Explain closures with a practical example.
IIFE captures loop variable
Closure mutation persists across calls
Wallet exposes stale primitive snapshot
What is the difference between call, apply, and bind?
Closure captures binding not snapshot
Curried closure
Shared closure state across methods
Function is called with arguments object
Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.