Intermediate13 questionsFull Guide

JavaScript Closure Interview Questions

Closures are the most frequently tested JavaScript concept. Master them with real interview questions, answers, and code examples.

The Mental Model

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.

The Explanation

How closures actually work — the spec-level mechanism

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.

Closures capture BINDINGS, not VALUES — the most misunderstood fact

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 ✓

Three fundamental closure patterns

Pattern 1 — Private state (data encapsulation)

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

Pattern 2 — Function factory (generating specialised functions)

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

Pattern 3 — Partial application (fix some arguments)

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)

Memoization — closures as a performance optimisation

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, debounce, throttle — closure-powered utilities

// 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)
    }
  }
}

The Revealing Module Pattern — closures as namespacing

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.

Stale closure — the most common React interview bug

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 and memory leaks

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.

Closure vs class — when to use each

AspectClosureClass
PrivacyTrue private — inaccessible outside by language designPrivate fields (#) require ES2022+; prototype methods are public by default
MemoryEach instance allocates a new Environment Record + function objectsMethods on prototype — shared across all instances, lower per-instance cost
InheritanceManual composition — explicit function callsBuilt-in prototype chain and extends syntax
PerformanceSlower for many instances (many function objects)Faster for many instances (shared prototype methods)
ReadabilitySimple for 1-3 methods; complex for larger APIsBetter for rich APIs with many methods
Use whenSmall, focused utilities; React hooks; functional styleComplex 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()

The "lost this" — closure and dynamic binding collision

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.

DevTools: [[Scopes]] — what interviewers mean when they ask

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.

Closure performance — what to know

Every time a function is created, JavaScript allocates:

  • A function object in the heap
  • An [[Environment]] reference to the current Lexical Environment

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.

Common Misconceptions

⚠️

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.

Where You'll See This in Real Code

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.

Interview Cheat Sheet

  • Every function is a closure — it carries [[Environment]] pointing to the Lexical Environment where it was written
  • Closures capture the BINDING (live reference), not the VALUE at creation time
  • Changing a captured variable in the outer scope: the closure sees the new value
  • Changing a captured variable inside the closure: the outer scope sees the mutation
  • var in loops: all closures share ONE binding → all see the final value
  • let in loops: new binding per iteration → each closure gets its own independent variable
  • Closures keep captured Environment Records alive in the heap → prevents GC of captured data
  • Memory leak risk: closure capturing DOM node or large object that's no longer needed
  • Fix memory leaks: set captured variables to null, or remove event listeners
  • Revealing module pattern: IIFE + return object = public API over private closure state
  • Memoization: cache Map lives in closure, persists across calls, independent per memoized function
  • Stale closure in React: closure captures state value at effect creation time, not at call time
  • Fix stale closure: functional setState update (prev => ...), add to deps array, or useRef
  • Arrow functions close over 'this' from the enclosing scope; regular functions do not
  • "Lost this" bug: pass regular function as callback → this becomes undefined/global
  • Fix "lost this": use arrow function, or capture this in a closure variable (const self = this)
  • Closure vs class: closures are truly private, classes have lower memory cost for many instances
  • DevTools [[Scopes]] / Scope panel shows each captured Environment Record in the chain
💡

How to Answer in an Interview

  • 1.Open with the precise definition: "A closure is a function bundled with a reference to its Lexical Environment — not a copy of values, but a live reference to the same bindings." This immediately separates you from candidates who say "a function inside a function." Follow with the backpack analogy only if the interviewer seems to want intuition over precision.
  • 2.The binding-vs-value distinction is the most differentiating point. Prepare the counter example where read() and write() share the same variable, then connect it to why the loop bug happens. Candidates who explain the mechanism (one var binding vs one let binding per iteration) at the spec level get hired at 30+LPA. Candidates who just say "use let" get junior roles.
  • 3.For React interviews, stale closures are almost always asked. Prepare all three fixes cold: functional setState, dependency array, useRef pattern. Know WHY each fix works in terms of closures — not just that it fixes the lint warning.
  • 4.When asked about memory leaks, bring up the detached DOM node pattern unprompted. Say "the most common closure memory leak I've seen is event listeners closing over DOM nodes that have been removed — the node can't be GC'd because the closure's Environment Record still references it." This shows production awareness.
  • 5.The "lost this" problem is a closure question disguised as a this question. Connect them explicitly: arrow functions solve "lost this" precisely because they have no own this — they close over the this of the enclosing regular function's execution context.
  • 6.If asked "how are closures used in real code" — don't just say React. List: memoization, debounce/throttle, once(), partial application, module pattern, event handlers, currying. Each one you name adds credibility.
📖 Deep Dive Articles
Arrow Functions vs Regular Functions in JavaScript: 6 Key Differences9 min readTop 50 JavaScript Interview Questions (With Deep Answers)18 min readJavaScript Scope Explained: Lexical Scope, Scope Chain, and How Variables Are Found10 min readJavaScript Hoisting Explained: What Actually Moves, What Doesn't, and Why It Matters9 min readJavaScript Prototypes Explained: The Object Model Behind Every Class and Method10 min readJavaScript `this` Keyword Explained: Five Rules, Zero Guessing10 min readJavaScript Closures: What They Actually Are (And Why Interviews Love Them)10 min readJavaScript Output Questions: 30 Tricky Examples That Test Real Understanding14 min read

Practice Questions

13 questions
#01

Classic var in loop with setTimeout

EasyClosures & Scope
FlipkartRazorpayZomato+1
#02
EasyClosures & ScopePRO

var in loop — all closures share one variable

#03

let in loop — each iteration gets own binding

EasyClosures & Scope
FlipkartRazorpaySwiggy+1
#04
MediumClosures & ScopePRO

Counter factory shares global state

#05

Explain closures with a practical example.

EasyCore JS💡 A function that remembers its outer scope after the outer function returns
#06

IIFE captures loop variable

MediumClosures & Scope
RazorpayGoogleCRED
#07

Closure mutation persists across calls

MediumClosures & Scope
RazorpayFlipkartShareChat
#08
EasyClosures & ScopePRO

Wallet exposes stale primitive snapshot

#09

What is the difference between call, apply, and bind?

EasyFunctions PRO💡 All set "this" — call=comma, apply=array, bind=returns new fn
#10

Closure captures binding not snapshot

MediumClosures & Scope
ShareChatMeesho
#11

Curried closure

MediumClosures & Scope
RazorpayCREDAtlassian
#12

Shared closure state across methods

HardClosures & Scope
GoogleCREDAtlassian
#13

Function is called with arguments object

HardClosures & Scope
FlipkartRazorpayZomato

Related Topics

JavaScript var, let, and const Interview Questions
Beginner·3–5 Qs
JavaScript "this" Keyword Interview Questions
Intermediate·6–10 Qs
JavaScript Hoisting Interview Questions
Intermediate·4–8 Qs
JavaScript Scope Interview Questions
Beginner·4–6 Qs
JavaScript Execution Context Interview Questions
Advanced·3–6 Qs
🎯

Can you answer these under pressure?

Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.

Practice Free →Try Output Quiz