Skip to content
JavaScript js scope-closures 4 min read

Closures

A closure is a function bundled together with references to the variables from the scope where it was defined. Because JavaScript functions remember their lexical environment, an inner function keeps access to its outer function’s variables even after that outer function has returned. Closures power everything from data privacy and factory functions to event handlers and module patterns — once you understand them, a huge amount of idiomatic JavaScript suddenly makes sense.

What a closure actually is

Every time a function is created, it captures the scope in which it was defined — not the scope from which it is later called. That captured scope stays alive as long as the inner function is reachable, even if the outer function has already finished executing and its call stack frame is gone.

function makeGreeter(greeting) {
  // `greeting` lives in makeGreeter's scope
  return function (name) {
    return `${greeting}, ${name}!`;
  };
}

const sayHi = makeGreeter("Hi");
const sayHola = makeGreeter("Hola");

console.log(sayHi("Ada"));
console.log(sayHola("Linus"));

Output:

Hi, Ada!
Hola, Linus!

makeGreeter has returned in both cases, yet the returned functions still see their own greeting. Each call to makeGreeter creates a fresh scope, so sayHi and sayHola close over independent values.

The classic counter

The canonical example of a closure is a counter that keeps state between calls without a global variable. The count variable is private to each counter instance — nothing outside can read or mutate it directly.

function createCounter(start = 0) {
  let count = start;
  return {
    increment: () => ++count,
    decrement: () => --count,
    value: () => count,
  };
}

const counter = createCounter(10);
console.log(counter.increment());
console.log(counter.increment());
console.log(counter.decrement());
console.log(counter.value());

Output:

11
12
11
11

All three returned methods close over the same count binding, so they share and mutate one piece of state. A second createCounter() call would produce an entirely separate count.

Private state and encapsulation

Closures are JavaScript’s oldest mechanism for true data privacy. Variables captured in a closure cannot be accessed from outside the function — there is no reference to reach them. This is the basis of the module pattern and a lightweight alternative to #private class fields.

function createBankAccount(initial) {
  let balance = initial;

  return {
    deposit(amount) {
      if (amount <= 0) throw new Error("Amount must be positive");
      balance += amount;
      return balance;
    },
    withdraw(amount) {
      if (amount > balance) throw new Error("Insufficient funds");
      balance -= amount;
      return balance;
    },
    get balance() {
      return balance;
    },
  };
}

const account = createBankAccount(100);
account.deposit(50);
account.withdraw(30);
console.log(account.balance);
console.log(account.balance === undefined); // no direct field access

Output:

120
false

There is no account.balance property storing a number — the getter reads the closed-over balance. The only way to change it is through the published methods, which enforce the rules.

The classic loop pitfall: var vs let

Before block scoping, the most common closure bug came from var in a loop. Because var is function-scoped, every closure created in the loop captured the same single variable, which by the time the callbacks ran held its final value.

// Buggy version with var
const fns = [];
for (var i = 0; i < 3; i++) {
  fns.push(() => i);
}
console.log(fns.map((fn) => fn()));

Output:

[3, 3, 3]

let fixes this because it creates a new binding for each iteration. Each closure captures its own i.

const fns = [];
for (let i = 0; i < 3; i++) {
  fns.push(() => i);
}
console.log(fns.map((fn) => fn()));

Output:

[0, 1, 2]

Always prefer let/const in loops. The var-based bug is so common it was a primary motivation for introducing block scoping in ES2015.

The table below summarizes why the behavior differs.

Aspectvar in looplet in loop
ScopeOne binding for the whole functionNew binding per iteration
Captured valueFinal value after loop endsValue at that iteration
Result above[3, 3, 3][0, 1, 2]
Fix neededWrap in IIFE per iterationNone

If you are stuck with var, the historical workaround was an IIFE to create a fresh scope each iteration: (function (j) { fns.push(() => j); })(i);.

Closures and memory

Because a closure keeps its captured scope alive, variables referenced by a long-lived closure will not be garbage collected. This is usually exactly what you want, but holding onto large objects (or DOM nodes) inside a closure that outlives its usefulness can cause leaks. Release the closure (e.g. remove the event listener) when you no longer need it.

Best practices

  • Reach for closures to encapsulate private state instead of leaking variables to a broader scope.
  • Use let/const in loops so each iteration captures its own binding — never var.
  • Return a small, intentional API from factory functions; expose only the methods callers need.
  • Remember that closures capture variables, not snapshots — a later mutation is visible to every closure sharing that binding.
  • Be mindful of memory: a closure pins its captured scope, so detach handlers and drop references you no longer use.
  • Prefer factory functions or #private class fields when they read more clearly than nested closures.
Last updated June 1, 2026
Was this helpful?