JavaScriptIntermediate

JavaScript Closures Explained

Learn JavaScript closures step by step: lexical environment, private state, factories, the loop pitfall and real use cases like counters and caching.

All JavaScript lessons

What you will learn

Closures have a scary reputation, but the idea is simple: a function remembers the variables around it, even after the code that created them has finished. Closures quietly power counters, private data, event handlers, caches and many popular patterns. In this lesson you will learn what a closure is, what the lexical environment has to do with it, how to use closures in real projects, and how to avoid the famous loop pitfall.

A quick recap

You need three ideas from earlier lessons:

  1. Functions are values. You can return a function from another function.
  2. Lexical scope. A function can use variables from the place where it was written.
  3. Local variables normally disappear. When a function finishes, its local variables are thrown away.

A closure is what happens when idea 2 and idea 3 meet in a surprising way.

What is a closure?

A closure is a function bundled together with the variables from the scope where it was created. The function carries those variables with it, wherever it goes.

Imagine that every function is born with a backpack. When a function is created inside another function, JavaScript puts the nearby variables into its backpack. Even if the outer function finishes and leaves, the inner function still has the backpack and can open it any time.

function createCounter() {
  let count = 0;

  return function increment() {
    count += 1;
    return count;
  };
}

const counter = createCounter();

console.log(counter());   // 1
console.log(counter());   // 2
console.log(counter());   // 3

Let us follow what happens:

  1. createCounter() runs. It creates a local variable count (set to 0).
  2. It returns the inner function increment.
  3. createCounter is now finished. Normally, count would be thrown away.
  4. But increment still uses count, so JavaScript keeps it alive in the function’s backpack.
  5. Every time you call counter(), it opens the backpack, adds 1 to count, and puts it back.

count is not global, and nobody outside can touch it. Only counter can. That is a closure.

The lexical environment

To understand how this works, you need one more term: the lexical environment.

Every time JavaScript runs a function (or a block), it creates a lexical environment. You can think of it as a small record with two parts:

  1. The variables of that scope (count: 0).
  2. A link to the outer environment (the scope that surrounds it).

A function also remembers which environment it was created in. That hidden link is what makes a closure.

counter  (the increment function)
   │
   └── remembers → { count: 0 }  ← environment of createCounter (kept alive)
                        │
                        └── outer → global environment { createCounter, counter }

When counter() runs and needs count, JavaScript follows this chain: first the function’s own variables, then the remembered environment (found count), and then the global one if needed. This is the scope chain from the scope lesson, and the only difference is that the remembered environment lives on after its function has returned.

Why it is called “lexical”: the link is decided by where the function is written in the code, not where it is called.

Closures capture variables, not values

The function does not copy the value at the moment it was created. It keeps a live link to the variable itself. If the variable changes later, the function sees the new value:

let message = "Hello";

function show() {
  console.log(message);
}

message = "Changed!";
show();   // "Changed!"

Every call creates a new, separate closure

Each call to createCounter() creates a brand-new count, so every counter is independent:

function createCounter() {
  let count = 0;
  return () => ++count;
}

const counterA = createCounter();
const counterB = createCounter();

console.log(counterA());   // 1
console.log(counterA());   // 2
console.log(counterB());   // 1  (B has its own count)
console.log(counterA());   // 3

This is the key to using closures: each closure owns its own private data.

Closures are everywhere

You do not have to return a function to create a closure. Any function that uses a variable from an outer scope and lives longer than that scope forms a closure. That includes callbacks and event handlers:

function showMessageLater(message) {
  setTimeout(() => {
    console.log(message);   // remembers "message" after showMessageLater is done
  }, 1000);
}

showMessageLater("Hello after one second");

showMessageLater finishes immediately, but the arrow function still remembers message one second later.

Why closures matter

Closures give you three big things:

  • Private state: variables that cannot be reached or damaged from outside.
  • Function factories: functions that build customised functions.
  • Memory for callbacks: handlers that remember values from when they were created.

Let us look at each one.

1. Private state

JavaScript has no private keyword for plain functions, but closures give you real privacy:

function createBankAccount(initialBalance) {
  let balance = initialBalance;   // private

  return {
    deposit(amount) {
      balance += amount;
      return balance;
    },
    withdraw(amount) {
      if (amount > balance) {
        throw new Error("Insufficient funds");
      }
      balance -= amount;
      return balance;
    },
    getBalance() {
      return balance;
    }
  };
}

const account = createBankAccount(100);

account.deposit(50);
account.withdraw(30);

console.log(account.getBalance());   // 120
console.log(account.balance);        // undefined (it is private!)

There is no way to read or change balance from outside, except through the three methods that close over it. Compare that with a plain object where anyone could write account.balance = 1000000.

2. Function factories

A factory is a function that creates other functions. Each created function remembers the argument you gave the factory:

function createMultiplier(factor) {
  return function (number) {
    return number * factor;
  };
}

const double = createMultiplier(2);
const tenTimes = createMultiplier(10);

console.log(double(7));      // 14
console.log(tenTimes(7));    // 70
function createGreeter(greeting) {
  return (name) => `${greeting}, ${name}!`;
}

const sayHello = createGreeter("Hello");
const sayNamaste = createGreeter("Namaste");

console.log(sayHello("Riya"));       // "Hello, Riya!"
console.log(sayNamaste("Karan"));    // "Namaste, Karan!"

3. Event handlers that remember

Each call to setupButton makes its own clicks variable, so every button counts separately:

<button id="btnA">Button A: 0 clicks</button>
<button id="btnB">Button B: 0 clicks</button>
function setupButton(id, label) {
  const button = document.getElementById(id);
  let clicks = 0;   // private to this button

  button.addEventListener("click", () => {
    clicks++;
    button.textContent = `${label}: ${clicks} clicks`;
  });
}

setupButton("btnA", "Button A");
setupButton("btnB", "Button B");

Without closures, you would need global variables like clicksA and clicksB, and a mess for every new button.

A common pitfall: closures inside loops

This is the classic interview question:

for (var i = 1; i <= 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Logs: 4, 4, 4

Why? Because var is function-scoped, there is only one i shared by the whole loop. All three callbacks close over the same variable. By the time the timers fire (after the loop is done), i has reached 4.

Fix 1: use let (the modern way)

for (let i = 1; i <= 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Logs: 1, 2, 3

let is block scoped, and in a for loop JavaScript creates a new i for every round. Each callback closes over its own i.

Fix 2: the old-school way, with a function

Before let, developers created a new scope using a function:

for (var i = 1; i <= 3; i++) {
  (function (copy) {
    setTimeout(() => console.log(copy), 100);
  })(i);
}
// Logs: 1, 2, 3

The IIFE receives the current i as copy, so each callback closes over a separate copy. You will still see this pattern in old code.

More real-life use cases

4. Run a function only once

function once(fn) {
  let alreadyRun = false;
  let result;

  return function (...args) {
    if (!alreadyRun) {
      alreadyRun = true;
      result = fn(...args);
    }
    return result;
  };
}

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

console.log(initialize());   // "Setting up..." then "ready"
console.log(initialize());   // "ready" (the setup did not run again)

The flag alreadyRun and the saved result live in the closure. This is useful for setup code, payments and any action that must not happen twice.

5. Caching results (memoization)

function memoize(fn) {
  const cache = {};   // private storage kept by the closure

  return function (n) {
    if (n in cache) {
      console.log("From cache");
      return cache[n];
    }

    console.log("Calculating...");
    cache[n] = fn(n);
    return cache[n];
  };
}

const slowSquare = (n) => n * n;      // imagine this is slow and heavy
const fastSquare = memoize(slowSquare);

console.log(fastSquare(9));   // "Calculating..." then 81
console.log(fastSquare(9));   // "From cache" then 81

The second time, the answer comes from the stored cache, not from a new calculation. Large apps use this idea to avoid repeating expensive work.

6. Limited attempts

function createLoginChecker(maxAttempts) {
  let attempts = 0;

  return function (password) {
    if (attempts >= maxAttempts) {
      return "Account locked";
    }

    attempts++;

    if (password === "secret123") {
      return "Welcome!";
    }
    return `Wrong password. ${maxAttempts - attempts} attempt(s) left.`;
  };
}

const login = createLoginChecker(3);

console.log(login("abc"));         // "Wrong password. 2 attempt(s) left."
console.log(login("xyz"));         // "Wrong password. 1 attempt(s) left."
console.log(login("secret123"));   // "Welcome!"

The attempt counter cannot be reset from outside.

7. A discount calculator factory

function createDiscount(percent) {
  return (price) => price - (price * percent) / 100;
}

const festivalSale = createDiscount(20);
const memberSale = createDiscount(10);

console.log(festivalSale(1000));   // 800
console.log(memberSale(1000));     // 900

8. The module pattern

Combine an IIFE with a closure to build a private “module”:

const cart = (function () {
  const items = [];   // private

  return {
    add(name, price) {
      items.push({ name, price });
    },
    total() {
      let sum = 0;
      for (const item of items) {
        sum += item.price;
      }
      return sum;
    },
    count() {
      return items.length;
    }
  };
})();

cart.add("Notebook", 80);
cart.add("Pen", 20);

console.log(cart.count());   // 2
console.log(cart.total());   // 100
console.log(cart.items);     // undefined (private)

Modern JavaScript offers real modules (import and export), but this pattern shows the principle behind them.

Coming soon: debouncing and throttling (in the performance lesson) are also built with closures. They remember a timer between calls.

Closures and memory

A variable kept by a closure stays in memory as long as the function that uses it exists. That is usually what you want, but be careful:

  • Do not keep huge data (big arrays, large objects) alive in a closure if you only need a small part of it.
  • Remove event listeners you no longer need, so their closures can be cleaned up.
  • When nothing refers to a function any more, JavaScript’s garbage collector automatically frees the function and its remembered variables.

For everyday code this is not a problem. It just helps to know that closures are not free.

Closures vs global variables

You could keep a counter in a global variable instead:

let count = 0;

function increment() {
  count++;
  return count;
}

It works, but any code can change count, you cannot have two independent counters, and name clashes become likely. A closure gives you many independent, protected counters with the same code.

Closures in interviews

Three questions come up again and again:

  1. “What is a closure?” A function together with the variables of the scope where it was created. It can use those variables even after that scope has finished.
  2. “What does this loop print?” (the var + setTimeout example). It prints 4, 4, 4, because all callbacks share one i. Using let prints 1, 2, 3.
  3. “How would you make a private variable?” Declare it inside a function and return only the functions that need access, such as createCounter or createBankAccount.

Common mistakes

  • Thinking a closure copies the value. It keeps a live link to the variable, so later changes are visible.

  • Using var in loops with callbacks. All callbacks share one variable. Use let.

  • Defining the “private” variable outside the factory. Then every user shares it:

    let count = 0;   // shared by ALL counters (a bug)
    
    function createBadCounter() {
      return () => ++count;
    }

    Put let count = 0; inside createCounter.

  • Leaking private data by returning it. If you return the inner array or object itself (return items;), outside code can change it. Return a copy or only methods.

  • Thinking closures only exist when you return a function. Callbacks and event handlers form closures too.

  • Keeping large data alive for no reason. Closures hold on to what they use.

  • Forgetting that each factory call is separate. counterA and counterB do not share their count.

  • Overusing closures. If a simple function with parameters does the job, keep it simple.

Practice

  1. Write createCounter() with increment, decrement and reset methods, plus getCount(). Test it, and make sure the count cannot be read directly.

  2. Create two counters and show that they do not affect each other.

  3. Write createMultiplier(factor), then make double, triple and tenTimes.

  4. Predict the output, then run it. Fix it so it prints 0, 1, 2:

    for (var i = 0; i < 3; i++) {
      setTimeout(() => console.log(i), 100);
    }
  5. Write createGreeter(greeting) so that createGreeter("Namaste")("Riya") gives "Namaste, Riya!".

  6. Build createWallet(startAmount) with add, spend (refuse if there is not enough money) and balance methods. Balance must be private.

  7. Write once(fn) and test it on a function that prints "Saved!". Call it three times.

  8. Write memoize(fn) and test it on a function that calculates a factorial. Show that the second call uses the cache.

  9. Click Practice in Editor and add a third button that counts clicks on its own, using setupButton.

  10. Challenge: write createRateLimiter(limit) that returns a function. The returned function should return true for the first limit calls and false after that.

Key takeaways

  • A closure is a function bundled with references to its surrounding state (its lexical environment).
  • The function keeps a live link to the outer variables, even after the outer function has finished.
  • Every call of a factory creates a new, independent closure.
  • Closures enable private variables, function factories, stateful callbacks, once, caching and the module pattern.
  • let in loops creates a new variable for every round, which avoids the shared-variable trap of var.
  • Closures hold their variables in memory, so keep what they capture small.

Once closures click, a lot of “how does this even work” moments in JavaScript start making sense.

Next, you will move on to arrays: JavaScript’s most important way of storing lists of data.