JavaScript · Functions

JavaScript Closures Explained — From Basics to Advanced Patterns

6 min read Updated 2026-06-17

Practice Closures interview questions

JavaScript closures, explained

Closures are one of the most powerful and most misunderstood features of JavaScript. They're behind data privacy, function factories, currying, memoization, debounce and throttle, and the famous var-in-a-loop bug. Once the underlying idea clicks, a whole class of "why does this happen?" questions answers itself. This guide goes from the core definition to the practical patterns built on it.

What a closure is

A 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.

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 crucial detail: a closure captures the variable (binding), not a snapshot of its value. So count is genuinely shared and mutable across calls. Technically every function is a closure; it only matters when the function outlives the scope it captured.

Lexical scope and the scope chain

"Lexical" means scope is determined by where code is written, not where it's called. When you reference a variable, the engine searches the current scope, then each enclosing scope outward, until the global scope — the scope chain.

function outer() {
  const x = 10
  function inner() { return x } // resolves x by lexical position
  return inner
}
outer()() // 10 — inner remembers outer's scope wherever it runs

Because scope is fixed at definition time, you can reason about what any closure can access just by reading the nesting. And because each call to an outer function creates a fresh scope, each returned closure captures independent state:

const a = counter()
const b = counter()
a(); a() // 2
b()      // 1 — separate closure, separate count

Value vs binding: the loop bug

The most famous closure question. With var (function-scoped), every iteration shares one binding, so deferred callbacks all read its final value. let (block-scoped) creates a fresh binding per iteration.

for (var i = 0; i < 3; i++) setTimeout(() => console.log(i)) // 3 3 3
for (let i = 0; i < 3; i++) setTimeout(() => console.log(i)) // 0 1 2

Before let, the fix was an IIFE that manufactured a new scope per iteration, capturing the value as a parameter:

for (var i = 0; i < 3; i++) ((j) => setTimeout(() => console.log(j)))(i) // 0 1 2

This is the same mechanism behind stale closures in callbacks and React renders: a function captures the values from the render/iteration it was created in, and never sees later changes unless it's recreated.

Data privacy and the module pattern

Variables in an enclosing scope are unreachable from outside — there's no reference to them except through the functions you expose. That's true encapsulation.

function account() {
  let balance = 0 // private
  return {
    deposit: n => (balance += n),
    withdraw: n => (balance -= n),
    get: () => balance,
  }
}
const a = account()
a.deposit(100)
a.get()     // 100
a.balance   // undefined — inaccessible

The module pattern (often an IIFE) uses this to keep private state and expose only a public API — the standard way to organize code before ES modules, and still useful for encapsulated singletons.

Function factories, currying, partial application

A function factory returns customized functions, each closing over its creation arguments:

const makeMultiplier = factor => n => n * factor
const double = makeMultiplier(2) // remembers factor = 2
double(5) // 10

Currying transforms a multi-argument function into a chain of single-argument ones, each closing over earlier args; partial application fixes some arguments now and returns a function for the rest. Both rely entirely on closures remembering the bound values.

const add = a => b => c => a + b + c
add(1)(2)(3) // 6

Stateful utilities: memoize, once, debounce, throttle

Closures shine when you need persistent private state between calls. A memoizer keeps a private cache:

function memoize(fn) {
  const cache = new Map()       // private, persists across calls
  return arg => cache.has(arg) ? cache.get(arg) : cache.set(arg, fn(arg)).get(arg)
}

Debounce and throttle hold a timer/timestamp in the closure. Debounce runs after activity stops; throttle runs at most once per interval:

function debounce(fn, delay) {
  let timer                      // remembered between calls
  return (...args) => {
    clearTimeout(timer)
    timer = setTimeout(() => fn(...args), delay)
  }
}

A once wrapper remembers whether it has run; an encapsulated counter exposes increment/reset methods over a private variable. All the same idea: state hidden in a closure.

Closures and this

A subtle point: closures capture variables, but this is not an ordinary 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
obj.arrow()()   // 'Ada'

The pre-arrow workaround const self = this captured this as a real closed-over variable.

Memory considerations

A closure keeps its captured scope alive as long as the closure is reachable. That's usually fine, but holding a closure that references large objects or DOM nodes — for example an event listener you never remove — retains that memory indefinitely.

const big = new Array(1e6)
el.addEventListener('click', () => console.log(big.length)) // pins `big`

Release references when done (remove listeners, null large objects), and consider WeakMap/WeakRef for caches so entries can be collected.

Recap

A closure is a function plus the lexical scope it remembers, captured by binding, not value — which explains the loop bug, stale closures, and shared vs independent state. That single mechanism powers data privacy (module pattern, private variables), function factories, currying and partial application, and stateful utilities like memoize, debounce, and throttle. Understand binding capture and lexical scope, mind the memory implications, and closures become a tool you reach for deliberately rather than a source of mystery.

Practice tests are coming soon

Get notified when interactive mock interviews and quizzes launch.