Skip to main content

🎒 Understanding Closures

A closure is a function that remembers the variables around it, even after the code that created them has finished running. It sounds abstract, but it is the quiet engine behind private data, React Hooks, and half the clever patterns you will meet in real JavaScript.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Define a closure and explain how lexical scoping and the scope chain make it possible
  • Use closures to build private state, function factories, and memoized functions
  • Diagnose and fix the two classic gotchas: loop variables and the this keyword
  • Recognize closures at work in React Hooks, event handlers, and Express middleware

Estimated Time: 35–45 minutes  •  Difficulty: Intermediate

Hands-on: Build a counter factory and a tiny event emitter that rely on closures for state.

In This Lesson

What Is a Closure?

A closure is the combination of a function together with the lexical environment in which it was declared. In plainer words: whenever you create a function, it quietly keeps a reference to the variables that were in scope where it was written — and it can still reach them later, even if the outer function has already returned.

🎒 The backpack analogy: Picture every function packing a backpack the moment it is created. Into that backpack go all the variables it could see at birth. Wherever the function travels afterwards — into an array, an event listener, a network callback — it carries the backpack with it and can always open it.

Here is the smallest example that shows the effect. Watch how inner still reads message long after outer has finished:

function outer() {
  const message = 'I was captured at creation time';

  function inner() {
    console.log(message); // reaches into outer's scope
  }

  return inner;           // outer returns and exits here
}

const remembered = outer();
remembered(); // "I was captured at creation time"

By the time remembered() runs, outer() has already returned. Normally its local variable message would be gone. But because inner closed over it, the variable lives on for as long as inner does. That is the whole idea — everything else in this lesson is a consequence of it.

A closure preserving access after the outer function returns The outer function's execution context holds a message variable. It returns an inner function, then the outer context is popped off the call stack, yet the inner function still reaches the preserved message when it runs later. outer() execution context message = "I was captured at creation time" inner() + its captured lexical environment return outer completes, popped off stack remembered() runs later — still reads message closure preserves access
Figure 1 — The outer context is gone, but the inner function keeps its backpack, so message survives.

How Closures Work

Closures fall out of three features working together. None of them is exotic; you have been using all three already.

1. Lexical scoping

JavaScript uses lexical (static) scoping: a function's scope is decided by where it is written in the source, not by where or when it is called. An inner function can always see the variables of the functions that physically enclose it.

const globalVar = 'global';

function outer() {
  const outerVar = 'outer';

  function inner() {
    const innerVar = 'inner';
    console.log(innerVar);  // own scope
    console.log(outerVar);  // parent scope
    console.log(globalVar); // global scope
  }

  inner();
}

2. The scope chain

When JavaScript looks up a name, it checks the current scope first, then the enclosing scope, then the next one out, all the way to the global scope. This ordered search is the scope chain.

flowchart TB A[Global scope: globalVar] --> B[outer scope: outerVar] B --> C[inner scope: innerVar] C -->|lookup misses locally, walks up| B B -->|still missing, walks up| A

3. Variables outlive their function

Normally a function's local variables are discarded when it returns. But if a closure still references them, the garbage collector leaves them alone. The count below is created once and then shared across every call:

function createCounter() {
  let count = 0; // created once, preserved by the closure

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

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

📖 Key Terms

Lexical environment: the set of variables in scope at the place a function is defined.

Scope chain: the ordered list of environments JavaScript searches when resolving a name.

Free variable: a variable a function uses but does not itself declare — exactly what a closure captures.

Private State & Function Factories

The most valuable everyday use of closures is encapsulation: keeping data private and exposing only the operations you choose. Because balance below lives inside the closure, nothing outside can read or corrupt it directly.

function createBankAccount(initialBalance) {
  let balance = initialBalance; // private — no outside access

  return {
    deposit(amount) {
      if (amount <= 0) return 'Invalid deposit amount';
      balance += amount;
      return `Deposited ${amount}. New balance: ${balance}`;
    },
    withdraw(amount) {
      if (amount <= 0 || amount > balance) return 'Invalid withdrawal amount';
      balance -= amount;
      return `Withdrew ${amount}. New balance: ${balance}`;
    },
    getBalance() {
      return balance;
    },
  };
}

const account = createBankAccount(100);
console.log(account.getBalance()); // 100
console.log(account.deposit(50));  // "Deposited 50. New balance: 150"
console.log(account.withdraw(30)); // "Withdrew 30. New balance: 120"
console.log(account.balance);      // undefined — genuinely private

Function factories

A factory is a function that returns customized functions. Each returned function closes over the arguments it was built with, so you can stamp out specialized behavior on demand:

function createGreeter(greeting) {
  return (name) => `${greeting}, ${name}!`;
}

const sayHello = createGreeter('Hello');
const sayHowdy = createGreeter('Howdy');

console.log(sayHello('Alice')); // "Hello, Alice!"
console.log(sayHowdy('Bob'));   // "Howdy, Bob!"

Memoization: closures for speed

A closure can hold a private cache that survives between calls, letting an expensive function skip work it has already done:

function memoize(fn) {
  const cache = new Map(); // private, persists across calls

  return function (...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}

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

const fastSquare = memoize(slowSquare);
console.log(fastSquare(9)); // computed once
console.log(fastSquare(9)); // returned instantly from cache

✅ The pattern to notice

In all three examples the shape is identical: an outer function declares some state, then returns an inner function (or object of functions) that reads and updates that state. The state is invisible from outside — that is encapsulation, achieved with nothing but a closure.

The Module Pattern

Before ES modules existed, developers used an Immediately Invoked Function Expression (IIFE) to create a private scope and expose a public interface. This "module pattern" is closures at scale, and you will still meet it in older codebases and library builds.

const calculator = (function () {
  // private
  let result = 0;
  const isValid = (n) => typeof n === 'number' && !Number.isNaN(n);

  // public interface
  return {
    add(n) { if (isValid(n)) result += n; return this; },
    subtract(n) { if (isValid(n)) result -= n; return this; },
    getResult() { return result; },
    reset() { result = 0; return this; },
  };
})();

calculator.add(5).subtract(2).add(10);
console.log(calculator.getResult()); // 13

The IIFE runs once, its local result and isValid never leak into the global scope, and the returned object's methods keep working because they closed over them. Returning this from each method is what enables the fluent method chaining you see above.

💡 From module pattern to ES modules

Today an import/export file gives you the same privacy for free — anything you do not export stays private to the module. The mental model, though, is the closure you just wrote by hand.

Common Gotchas

1. Loop variables with var

The most famous closure bug: create functions inside a loop with var, and every one of them shares the same variable. By the time they run, the loop has finished and the variable holds its final value.

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

// Fix: let is block-scoped — each iteration gets its own i
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// 0, 1, 2

⚠️ Why let fixes it

With var there is one i for the whole loop, so all closures capture the same box. With let, JavaScript creates a fresh binding of i for every iteration, so each closure captures its own value. Before ES6, developers wrapped the body in an IIFE — let made that ceremony obsolete.

2. The this keyword

this is not captured by an ordinary closure — a regular inner function gets its own this, which is usually undefined in strict mode. Arrow functions are different: they have no this of their own and inherit it lexically, which is exactly what you want here.

const user = {
  name: 'Alice',
  greetLater() {
    // Arrow function inherits `this` from greetLater
    setTimeout(() => {
      console.log(`Hi, I'm ${this.name}`);
    }, 100);
  },
};

user.greetLater(); // "Hi, I'm Alice"

// A regular function would have lost `this`:
// setTimeout(function () { console.log(this.name); }, 100); // undefined

3. Accidental memory retention

A closure keeps its captured variables alive. If it captures something large and lives a long time (say, an event listener that is never removed), that memory cannot be freed. The cure is to release the reference when you are done.

function makeProcessor() {
  const big = new Array(1_000_000).fill('data');
  return (i) => big[i];
}

let read = makeProcessor(); // `big` is retained by the closure
// ...use it...
read = null; // now `big` can be garbage collected

Closures in Modern JavaScript

Closures did not fade away when classes and modules arrived — they moved under the surface of the tools you use every day.

React Hooks are closures

useState works because the setter and the value close over a slot that persists between renders. A stripped-down mental model:

// Greatly simplified — the real React tracks slots per component
function useState(initialValue) {
  let value = initialValue;
  const setValue = (next) => {
    value = next;
    rerender(); // schedule a re-render
  };
  return [value, setValue];
}

Express middleware factories

Configurable middleware is a function factory: options are captured in a closure, and the returned middleware reads them on every request.

function requireRole(role) {
  // `role` is captured for the life of this middleware
  return function (req, res, next) {
    if (req.user?.role !== role) {
      return res.status(403).json({ message: 'Insufficient permissions' });
    }
    next();
  };
}

app.get('/admin', requireRole('admin'), (req, res) => {
  res.json({ message: 'Admin dashboard' });
});

💡 Private class fields vs. closures

Modern classes offer real privacy with the # prefix (#count). It is a great option for object-oriented code, but closures remain the go-to for standalone functions, callbacks, and anywhere you are not writing a class.

Hands-on Exercise

🏋️ Build a Counter Factory and a Tiny Event Emitter

Objective: Prove to yourself that closures preserve independent, private state.

Instructions:

  1. Write makeCounter(start = 0) that returns an object with increment(), decrement(), and value(). Each counter must keep its own count.
  2. Create two counters and confirm that operating on one never affects the other.
  3. Write createEmitter() that returns on(event, fn) and emit(event, ...args), storing listeners in a private object captured by the closure.
  4. Register two listeners for a 'ping' event and emit it once — both should fire.
💡 Hint

For the counter, declare let count = start; inside makeCounter and have each returned method read and update it. For the emitter, keep const listeners = {}; and push functions into listeners[event].

✅ Sample solution
function makeCounter(start = 0) {
  let count = start; // private state per counter
  return {
    increment() { return ++count; },
    decrement() { return --count; },
    value() { return count; },
  };
}

const a = makeCounter();
const b = makeCounter(100);
a.increment();      // 1
b.increment();      // 101
console.log(a.value(), b.value()); // 1 101 — independent

function createEmitter() {
  const listeners = {}; // private registry
  return {
    on(event, fn) {
      (listeners[event] ??= []).push(fn);
    },
    emit(event, ...args) {
      (listeners[event] || []).forEach((fn) => fn(...args));
    },
  };
}

const bus = createEmitter();
bus.on('ping', (msg) => console.log('A got', msg));
bus.on('ping', (msg) => console.log('B got', msg));
bus.emit('ping', 'hello');
// A got hello
// B got hello

Best Practices

✅ Do

  • Reach for closures to keep state private instead of using shared globals.
  • Use let/const in loops so each iteration captures its own binding.
  • Use arrow functions when you need an inner function to inherit this.
  • Null out or remove long-lived closures (event listeners, timers) when you are done with them.

⚠️ Don't

  • Don't capture large data structures in closures that outlive their usefulness — that is a memory leak.
  • Don't expect a regular inner function to inherit the outer this; it won't.
  • Don't reuse a single var across loop iterations and expect per-iteration values.
  • Don't hide so much behind closures that debugging becomes guesswork — keep captured state small and named clearly.

Summary & Quiz

🎉 Key Takeaways

  • A closure is a function plus the lexical environment it captured at creation.
  • Closures let variables outlive the function that created them, enabling private state.
  • They power function factories, memoization, and the module pattern.
  • The classic bugs are loop variables with var (fix with let) and this (fix with arrow functions).
  • React Hooks and Express middleware are closures in disguise.

🎯 Quick Quiz

Question 1: What exactly does a closure "close over"?

Question 2: Why do setTimeout callbacks in a var loop all print the final value?

Question 3: Which technique keeps a bank balance truly private in plain JavaScript?

📚 Further Reading

🚀 What's Next?

Closures are the foundation for treating functions as values you can pass and return. Next we build directly on them to explore higher-order function patterns — map, compose, curry, decorators, and more.

🎉 Nicely done!

You now understand the backpack every JavaScript function carries. Let's put it to work.