RizTech Academy logo
RizTech Academy
Functions, Scope and ClosuresLesson 3 of 530 min

Closures, explained with a problem they solve

Closures have a reputation for being the hard bit. They are not hard; they are usually taught backwards — definition first, use later. So here is the problem first.

The problem

You need a counter. Order numbers for the day, say, starting at 1 and never repeating.

The obvious approach:

let orderNumber = 0;

function nextOrder() {
  orderNumber += 1;
  return orderNumber;
}

console.log(nextOrder());
console.log(nextOrder());
1
2

It works, and it has a real flaw: orderNumber is sitting at the top level where anything can reach it. Any other code in your file — or any other file, later — can write orderNumber = 500 or reset it to zero, and nextOrder cannot stop them. The thing the counter depends on is the one thing it does not control.

What you want is a variable only nextOrder can touch.

The solution

Put it inside a function, and return the inner function:

function makeCounter() {
  let count = 0;

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

const nextOrder = makeCounter();

console.log(nextOrder());
console.log(nextOrder());
console.log(nextOrder());
1
2
3

Look at what just happened. makeCounter() ran and finished. Its local count should be gone — and yet the returned function keeps counting up from where it left off.

That is a closure: a function together with the scope it was created in. The inner function still needs count, so count is kept alive for exactly as long as that inner function exists. Nothing else in the programme can reach it. There is no name for it outside makeCounter.

It follows directly from lexical scope, from the last lesson: what a name means is decided by where the function was written. The inner function was written inside makeCounter, so count is what it means, for good.

Each call makes a fresh one:

const counterA = makeCounter();
const counterB = makeCounter();

console.log(counterA(), counterA(), counterA());
console.log(counterB());
1 2 3
1

Two independent counters, two independent count variables. counterB is not affected by counterA at all.

Private data, without classes

That pattern generalises. Here is a tiffin subscription with a balance nobody can edit directly:

function makeSubscription(startingBalance, ratePerPlate) {
  let balance = startingBalance;

  return {
    deliver(plates) {
      const cost = plates * ratePerPlate;
      if (cost > balance) {
        return 'Insufficient balance.';
      }
      balance -= cost;
      return `Delivered ${plates}. ₹${balance} left.`;
    },
    check() {
      return balance;
    },
  };
}

const priya = makeSubscription(500, 80);

console.log(priya.deliver(3));
console.log(priya.deliver(3));
console.log(priya.deliver(3));
console.log(priya.check());
console.log(priya.balance);
Delivered 3. ₹260 left.
Delivered 3. ₹20 left.
Insufficient balance.
20
undefined

priya.balance is undefined — there is no such property. The only ways to change the balance are deliver, which checks first, and nothing else. You cannot set it to a million, and you cannot make it negative.

This is what closures are actually for: state that is remembered between calls and cannot be reached from outside. Classes do something similar and arrive in module 5; closures got there first and are still the lighter tool.

The classic bug

This is the single most famous JavaScript interview question, and now you have everything needed to understand it.

const callbacks = [];

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

console.log(callbacks.map((fn) => fn()));
[ 3, 3, 3 ]

Three functions, all returning 3. Nobody wanted that.

The reason is module 2's var leak meeting closures. var i is scoped to the whole function, so all three closures captured the same i — not three copies of a value, one shared variable. By the time any of them ran, the loop had finished and that one variable held 3.

Change one word:

const callbacks = [];

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

console.log(callbacks.map((fn) => fn()));
[ 0, 1, 2 ]

let creates a fresh binding for every iteration, so each closure captured its own. This is a specific, deliberate rule for let in for loops, and it exists precisely because the var version caught so many people.

You will meet this for real with setTimeout and with event handlers in a loop — module 7 — where the functions genuinely run later. The fix is always the same: use let.

Closures capture variables, not values

Worth being precise, because it explains the bug above rather than just patching it.

let rate = 80;

const showRate = () => rate;

rate = 95;
console.log(showRate());
95

showRate did not photograph rate when it was created. It kept a live reference. Change the variable afterwards and the closure sees the new value.

That is usually what you want. It is occasionally a surprise, and when you need a snapshot, take a copy inside a function:

function freeze(value) {
  return () => value;
}

let rate2 = 80;
const frozen = freeze(rate2);
rate2 = 95;
console.log(frozen());
80

value is a parameter — a fresh variable set once, at call time.

The cost

Closures are not free, and the honest version is this: a closure keeps its entire enclosing scope alive for as long as the function exists. If a closure captures one small number from a function that also held a large array, that array may be kept in memory too.

For the code you will write this is almost never a problem, and worrying about it early will make your code worse rather than faster. It becomes real in long-lived pages that create many closures — a handler attached on every render and never removed. Module 7 covers removing event listeners, which is the practical form this takes.

Check your work

makeCounter() returns a function that counts 1, 2, 3. count survives because the returned function still refers to it, and it is unreachable from anywhere else.

Two counters from makeCounter() are independent — 1 2 3 and then 1. Each call creates a new scope with its own count.

priya.balance is undefined. The object returned exposes only deliver and check; balance is a closed-over variable, not a property. That is the privacy.

It is the third deliver(3) that fails, not the second. Three plates at ₹80 is ₹240. From ₹500: the first leaves ₹260, the second leaves ₹20, and the third is refused because ₹240 is more than ₹20. If you assumed two deliveries would exhaust ₹500 without doing the arithmetic, this is the cheapest possible place to learn to check.

priya.balance = 100000 does not change the balance. check() still returns 20. The assignment quietly adds a new, unrelated property called balance to the returned object, which nothing reads. The closed-over variable is untouched — and note that the assignment did not fail, it just had no effect, which is the more dangerous outcome of the two.

The var loop gives [3, 3, 3]. All three closures share one i, which is 3 once the loop ends. With let it is [0, 1, 2], because let creates a new binding each iteration.

showRate() after rate = 95 prints 95. Closures capture the variable, not a copy of its value at creation time.

frozen() prints 80, because value is a parameter — a separate variable assigned once when freeze was called.

makeRateCard has to hand back a copy of the history:

function makeRateCard() {
  const history = [];
  let current = null;

  return {
    set(rate) {
      current = rate;
      history.push(rate);
    },
    get() {
      return current;
    },
    history() {
      return [...history];
    },
  };
}

return history would look identical and be broken. An array is handed back by reference, so a caller doing card.history().push(9999) would be pushing onto your private array — closure or not. [...history] returns a fresh copy, and pushing to that changes nothing. This is const protecting the binding and not the contents, from module 2, showing up where it actually costs something.

Practice

  1. Build makeCounter and call the returned function five times.
  2. Create two counters from it and interleave their calls. Confirm they do not affect each other.
  3. Try to reach the inner count from outside. Confirm you cannot, and say what that buys you.
  4. Build makeSubscription and deliver until the balance runs out. Predict which call fails before you run it — the arithmetic is the point.
  5. Try priya.balance = 100000 and then priya.check(). Explain the result.
  6. Write the var loop bug. Get [3, 3, 3], then change one word and get [0, 1, 2]. Say out loud what the three closures were sharing.
  7. Prove closures capture variables, not values: create a closure over a let, change the variable, and call the closure.
  8. Write freeze(value) and show it does not change when the original does.
  9. Harder. Write makeRateCard() returning { set, get, history } where set(rate) records a new rate, get() returns the current one, and history() returns every rate ever set, in order. Nothing outside may modify the history — check that returning the array directly lets a caller push to it, and work out how to prevent that.

Next: this — the other thing everyone finds confusing, and the one arrow functions were invented to fix.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship