Understanding JavaScript Closures

by Kuligaposten 2026-08-19

A closure is just a function that remembers the scope it was created in — here is why that matters.

Understanding JavaScript Closures

A closure happens whenever a function is defined inside another function and keeps access to the outer function's variables, even after the outer function has finished running. It sounds abstract until you see it — then it's obvious you've been using them all along.

The minimal example

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

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

makeCounter() returns, but the inner function still has access to count. Each call to counter() reads and updates the same count variable — it isn't reset, because the closure keeps it alive.

Every call creates a new closure

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

console.log(counterA()); // 1
console.log(counterA()); // 2
console.log(counterB()); // 1 — a separate count, unaffected by counterA

counterA and counterB each close over their own count, because makeCounter() created a fresh variable on each call.

The classic loop bug

This is the closure gotcha almost everyone hits at least once:

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

var is function-scoped, so all three callbacks close over the same i — by the time the timeouts fire, the loop has already finished and i is 3. Switching to let fixes it, because let creates a new binding for i on every iteration:

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

Practical use: private state

Closures are how you get private variables in JavaScript without a class:

function createBankAccount(initialBalance) {
  let balance = initialBalance;

  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);
console.log(account.getBalance()); // 150

There's no way to reach balance from outside — the only access is through the methods the closure exposes. That's real encapsulation, not just a naming convention like _balance.

Practical use: memoization

function memoize(fn) {
  const cache = new Map();
  return (arg) => {
    if (cache.has(arg)) return cache.get(arg);
    const result = fn(arg);
    cache.set(arg, result);
    return result;
  };
}

const slowSquare = (n) => {
  for (let i = 0; i < 1e8; i++); // pretend this is expensive
  return n * n;
};

const fastSquare = memoize(slowSquare);
fastSquare(5); // computed
fastSquare(5); // returned from cache

The cache map lives only inside the closure memoize returns — every memoized function gets its own private cache, with zero risk of collisions between different memoized functions.

Back to Home