Core Concepts10 min read · Updated 2025-06-01

JavaScript Closures: What They Actually Are (And Why Interviews Love Them)

Closures aren't a quirky JavaScript feature — they're the mechanism that makes most of the language work. Here's the model that makes them click, plus every closure question you'll face in an interview.

💡 Practice these concepts interactively with AI feedback

Start Practicing →

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.

📚 Practice These Topics
Hoisting
4–8 questions
Closure
8–12 questions
Scope
4–6 questions
Execution context
3–6 questions

Put This Into Practice

Reading articles is passive. JSPrep Pro makes you actively recall, predict output, and get AI feedback.

Start Free →Browse All Questions

Related Articles

Core Concepts
map() vs forEach() in JavaScript: Which One to Use and Why It Matters
7 min read
Core Concepts
Arrow Functions vs Regular Functions in JavaScript: 6 Key Differences
9 min read
Interview Prep
Promise.all vs allSettled vs race vs any: The Complete Comparison
9 min read