JavaScript Closures

A Function That Remembers

JavaScript Closures

When a function references variables from an outer scope, it forms a closure — it remembers those variables even after the outer function has returned.

5 min read Level 3/5 #closures#scope#functions
What you'll learn
  • Recognize a closure
  • Use closures to make private state
  • Avoid the loop-variable closure pitfall

A closure is a function plus the variables it remembers from the scope where it was created. Closures are how a function “keeps state” between calls.

A function that remembers script.js
function makeCounter() {
  let count = 0;
  return function () {
    count++;
    return count;
  };
}

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

What’s happening:

  1. makeCounter declares count and returns an inner function.
  2. The inner function references count — it forms a closure over the variable.
  3. Each time we call counter(), the same count is updated.
  4. The outside world can’t access count directly — it’s “private”.

Private State Without Classes

Closures are the original way to encapsulate state in JavaScript.

Private balance script.js
function makeAccount(initial) {
  let balance = initial;
  return {
    deposit: (n) => (balance += n),
    withdraw: (n) => (balance -= n),
    getBalance: () => balance,
  };
}

const acct = makeAccount(100);
acct.deposit(50);
console.log(acct.getBalance()); // 150

acct.withdraw(30);
console.log(acct.getBalance()); // 120

// console.log(acct.balance);  // undefined — no direct access
▶ Preview: console

Closures Capture the Variable, Not Its Value

This trips people up. The closure captures the variable binding, not a snapshot of the value at definition time.

Closures share variables script.js
function makeBoth() {
  let value = 0;
  return {
    get: () => value,
    set: (v) => (value = v),
  };
}

const both = makeBoth();
console.log(both.get()); // 0

both.set(42);
console.log(both.get()); // 42  ← same `value` shared by both functions
▶ Preview: console

get and set both close over the same value. Updating it through one is visible to the other.

The Classic var Closure Trap

A for loop with var shares the same loop variable across all iterations. Closures created inside the loop all point at the same var.

The var trap script.js
const handlers = [];

for (var i = 0; i < 3; i++) {
  handlers.push(() => console.log(i));
}

handlers[0]();  // 3   😱
handlers[1]();  // 3   😱
handlers[2]();  // 3   😱
▶ Preview: console

All three closures share the same i, which is 3 by the time the loop ends. Fix: use let, which gives each iteration its own i.

let to the rescue script.js
const handlers = [];

for (let i = 0; i < 3; i++) {
  handlers.push(() => console.log(i));
}

handlers[0]();  // 0
handlers[1]();  // 1
handlers[2]();  // 2
▶ Preview: console

This single difference — let over var — eliminates a whole class of bugs that plagued JavaScript for over a decade.

Closures Are Everywhere

You’ve been using closures without realizing:

  • setTimeout(() => doSomething(x), 1000) — the callback closes over x.”
  • addEventListener("click", () => updateUI(state)) — closes over state.
  • array.map((item) => item * multiplier) — closes over multiplier.

Closures are foundational. Once they click, they make a lot of JavaScript patterns clearer.

Try It Yourself

Exercise

A counter factory

Difficulty 3/5~4 min
Write `makeCounter()` — a function that returns a counter function. Each time the returned function is called, it should return one more than the last time, starting at `1`. Two counters made from `makeCounter()` should be **independent**. Verify by logging: ``` const a = makeCounter(); a(); a(); a(); // returns 1, 2, 3 const b = makeCounter(); b(); // returns 1 (independent) ```
solution.js
// declare makeCounter here

const a = makeCounter();
console.log(a(), a(), a());

const b = makeCounter();
console.log(b());
3tests will run
💡 Show hint
Inside `makeCounter`, declare `let count = 0`. Return a function that does `count++; return count;`. The returned function closes over `count`, and each call to `makeCounter` creates a fresh `count`.
✅ Show solution
function makeCounter() {
  let count = 0;
  return function () {
    count++;
    return count;
  };
}

const a = makeCounter();
console.log(a(), a(), a());

const b = makeCounter();
console.log(b());

Up Next

A function calling itself — recursion.

JavaScript Recursion →