Closures

Understand how a function keeps access to the variables around it, and use that to build private state, factories and event handlers.

  • Beginner
  • 7 min read
  • Updated 17 Oct 2026

What you will learn

  • Explain how a closure works
  • Use closures for private state
  • Build function factories that preconfigure behaviour
  • Recognise and fix common closure bugs

Before you start

A closure is a function together with the variables it could see when it was created. Closures are why event handlers can use variables from the code that set them up, and they are the simplest way to keep data private in JavaScript. You have already used them without naming them.

A function remembers where it was born

In the last lesson you saw that scope is lexical: a function sees the variables around the place it was written. A closure is what happens when that function outlives the scope that created it.

JavaScript
function makeGreeter(greeting) {
  return function (name) {
    return `${greeting}, ${name}`;
  };
}

const hello = makeGreeter('Hello');
const hiya = makeGreeter('Hiya');

console.log(hello('Sam')); // → Hello, Sam
console.log(hiya('Sam'));  // → Hiya, Sam

makeGreeter has finished running by the time hello('Sam') is called. Its parameter greeting would normally be gone. It survives because the returned function still refers to it. Each call to makeGreeter creates a fresh scope, so hello and hiya each keep their own greeting.

The key idea: a closure keeps a live link to the variable, not a copy of its value. If the variable changes, the closure sees the change.

JavaScript
function makeCounter() {
  let count = 0;
  return () => {
    count += 1;
    return count;
  };
}

const next = makeCounter();
console.log(next()); // → 1
console.log(next()); // → 2
console.log(next()); // → 3

const other = makeCounter();
console.log(other()); // → 1

count lives on between calls because next holds on to it. other has its own separate count.

Private state

Nothing outside makeCounter can reach count directly. The only way to change it is through the functions you choose to return. That makes closures a natural tool for private state:

JavaScript
function createWallet(startingBalance) {
  let balance = startingBalance;

  return {
    deposit(amount) {
      if (amount <= 0) throw new RangeError('Deposit must be positive');
      balance += amount;
    },
    spend(amount) {
      if (amount > balance) return false;
      balance -= amount;
      return true;
    },
    getBalance() {
      return balance;
    },
  };
}

const wallet = createWallet(50);
wallet.deposit(25);
console.log(wallet.spend(100));   // → false
console.log(wallet.getBalance()); // → 75
console.log(wallet.balance);      // → undefined

The three methods share one balance. Code that uses the wallet cannot set the balance to a negative number or a string, because it never touches the variable. Classes now offer private fields with # for the same job (lesson 36), but closures remain common and work everywhere.

Function factories

A factory is a function that returns a preconfigured function. It saves you from passing the same argument again and again:

JavaScript
function formatWith(currency) {
  const formatter = new Intl.NumberFormat('en-GB', { style: 'currency', currency });
  return (amount) => formatter.format(amount);
}

const gbp = formatWith('GBP');
const eur = formatWith('EUR');

console.log(gbp(12.5)); // → £12.50
console.log(eur(12.5)); // → €12.50

The formatter is built once per currency and reused on every call. Lesson 27 covers Intl properly.

Closures in event handlers

Every event listener that uses a variable from the surrounding code is a closure. This is how a handler knows which item it belongs to:

JavaScript
function setUpLikeButton(button, postTitle) {
  let likes = 0;

  button.addEventListener('click', () => {
    likes += 1;
    button.textContent = `Like (${likes})`;
    console.log(`${postTitle} now has ${likes} likes`);
  });
}

document.querySelectorAll('.like-button').forEach((button) => {
  setUpLikeButton(button, button.dataset.title);
});

Each button gets its own likes and postTitle, held by its own handler.

Run once

Closures can remember whether something has already happened:

JavaScript
function once(fn) {
  let called = false;
  let result;
  return (...args) => {
    if (!called) {
      called = true;
      result = fn(...args);
    }
    return result;
  };
}

const initialise = once(() => {
  console.log('Setting up');
  return 'ready';
});

console.log(initialise()); // → Setting up, then → ready
console.log(initialise()); // → ready

Wrapping a function to add behaviour like this is a common pattern. Debounce and throttle in lesson 48 use the same idea.

Common closure bugs

The shared loop variable

This is the classic closure bug, and it only happens with var:

JavaScript
const buttons = [];
for (var i = 0; i < 3; i += 1) {
  buttons.push(() => console.log(`Button ${i}`));
}
buttons[0](); // → Button 3

There is only one i for the whole loop, and all three closures share it. By the time any of them runs, i is 3. The fix is let, which gives each iteration its own binding:

JavaScript
const fixed = [];
for (let i = 0; i < 3; i += 1) {
  fixed.push(() => console.log(`Button ${i}`));
}
fixed[0](); // → Button 0

Looping with forEach or for...of also avoids the problem, because each callback or iteration has its own variable.

Expecting a snapshot

Because closures link to variables, not values, a closure that runs later sees the latest value:

JavaScript
let status = 'loading';
const report = () => console.log(`Status: ${status}`);

status = 'done';
report(); // → Status: done

If you need the value at a particular moment, copy it into a const inside a new scope at that moment, or pass it as an argument.

Holding on to large data

A closure keeps everything it refers to alive. A handler that refers to a large array keeps that array in memory for as long as the handler exists. That is usually fine. It becomes a leak when listeners or timers are never removed. Lesson 47 shows how to find and fix this.

JavaScript
function attachReport(button, bigDataset) {
  const total = bigDataset.length;       // keep only what you need
  button.addEventListener('click', () => {
    console.log(`Rows: ${total}`);       // bigDataset is not referenced here
  });
}

Modern engines generally keep only the variables a closure actually uses, so extracting the small value you need lets the large one be collected.

Summary

  • A closure is a function plus the variables in scope where it was created.
  • Closures keep a live link to variables, so they see later changes.
  • Each call to an outer function creates new variables, so each returned closure has its own state.
  • Use closures for private state, factories, run-once wrappers and event handlers.
  • Avoid the shared-loop bug with let, and do not hold large data in long-lived closures.

Further reading