Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

A closure lets a function keep using bindings from the scope where it was created, even after that surrounding function has returned. For example, makeCounter() can return a function that increments a local count each time you call it: the returned function still has access to that count.

How lexical scope explains closures

JavaScript resolves a function’s variable names according to where the function was defined, not where it is later called. This rule is lexical scoping. A function nested inside another function can refer to bindings in the outer function’s scope.

For instance, inner can read message because it was defined inside the scope where message exists:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
function outer() {
  const message = "Hello";

  function inner() {
    return message;
  }

  return inner;
}

const sayHello = outer();
console.log(sayHello()); // "Hello"

Although outer() has finished running, sayHello() can still resolve message. The function and its access to relevant surrounding state form a closure. MDN describes a closure as “the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment).” See the MDN JavaScript Guide to closures.

A lexical environment is the surrounding context in which names are defined and resolved. A closure is not simply an anonymous function, nor is it limited to functions returned by other functions. A nested function or callback can use outer bindings whenever lexical scope makes them available.

Closures retain access to bindings, not frozen snapshots

It is tempting to imagine that a closure copies the value of every outer variable when the function is created. That is not the right mental model: a closure retains access to bindings it can resolve. If the binding changes, a later call can observe the changed value.

function makeReader() {
  let status = "waiting";

  return {
    read() {
      return status;
    },
    update(nextStatus) {
      status = nextStatus;
    }
  };
}

const task = makeReader();
console.log(task.read()); // "waiting"
task.update("done");
console.log(task.read()); // "done"

Both methods refer to the same status binding created by one call to makeReader(). That shared access is useful when a function needs to read or update state later.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Function factories create separate environments

A function factory returns functions configured by the values in its own call. MDN’s makeAdder pattern demonstrates this:

function makeAdder(x) {
  return function (y) {
    return x + y;
  };
}

const add5 = makeAdder(5);
const add10 = makeAdder(10);

console.log(add5(2));  // 7
console.log(add10(2)); // 12

Each call to makeAdder creates a distinct environment for its parameter x. The functions share the same function-body structure, but add5 resolves x from the call that supplied 5, while add10 resolves it from the call that supplied 10.

Closures can expose operations over private state

A closure can keep a local binding out of direct reach while returning functions that operate on it. This supports a small counter interface:

function createCounter() {
  let count = 0;

  return {
    increment() {
      count += 1;
      return count;
    },
    current() {
      return count;
    }
  };
}

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.current());   // 1

The methods share the environment created by that particular call to createCounter(). Code using counter can call the provided operations, but it cannot directly access the local count binding.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Why closures matter in callbacks

Callbacks often run later than the code that creates them. If a callback refers to an outer binding, lexical scope determines which binding it can use when it eventually runs. That is why closures are useful for callbacks as well as function factories and stateful interfaces.

When a callback produces an unexpected value, trace the function back to where it was defined. The place where it is invoked does not change which outer binding its names resolve to.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

The classic loop closure problem: var versus let

var is function-scoped (or global when declared at top level), not block-scoped. In a loop, callbacks created with a var loop variable can all refer to the same binding. If those callbacks run after the loop ends, they observe that binding’s final value rather than a separate value for each iteration.

const callbacks = [];

for (var i = 0; i < 3; i++) {
  callbacks.push(function () {
    return i;
  });
}

console.log(callbacks[0]()); // 3
console.log(callbacks[1]()); // 3
console.log(callbacks[2]()); // 3

Here, each callback resolves the same function-scoped i. A loop declaration with let provides a per-iteration binding, so each callback can use its iteration’s value:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const callbacks = [];

for (let i = 0; i < 3; i++) {
  callbacks.push(function () {
    return i;
  });
}

console.log(callbacks[0]()); // 0
console.log(callbacks[1]()); // 1
console.log(callbacks[2]()); // 2

Other modern alternatives include iterating with for...of or using forEach(), where the callback receives an iteration value. MDN explains the loop behavior and these alternatives in its closures guide.

Closure cheat sheet

  • Definition: a function together with access to relevant references in its surrounding lexical environment.
  • Scope rule: names resolve according to where a function is defined, not where it is called.
  • After a return: an inner function can still use outer bindings it refers to.
  • Separate factory calls: each call can create a separate environment, as with makeAdder(5) and makeAdder(10).
  • Shared state: functions created in one environment can share access to the same binding.
  • Loop caution: callbacks in a var loop can share one binding; let loop declarations create per-iteration bindings.

For debugging, ask: Which binding does this function resolve here? Then locate where the function was defined and check whether other functions share or modify that binding.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.