AlgoInsight

Free lesson · JavaScript Basic to Advance

Closures for Private State

Easy5 min read

Using Closures Deliberately

The previous Topic established what closures are. This Topic is about what they let you build. Closures are the foundational mechanism for creating private state in JavaScript — variables that can't be accessed from outside, only through carefully controlled functions.

function createBankAccount(initialBalance) {
  let balance = initialBalance;   // private — no one can touch this directly

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

const account = createBankAccount(100);

console.log(account.getBalance());    // 100
console.log(account.deposit(50));      // 150
console.log(account.withdraw(80));     // 70
console.log(account.withdraw(200));    // "Insufficient funds"

// console.log(account.balance);    // undefined — there's no 'balance' property
// account.balance = 999999;         // doesn't work — no direct access
console.log(account.getBalance());    // still 70

The balance variable is a private value. Three functions — deposit, withdraw, and getBalance — share access to it through closures. From outside the account object, there's no way to read or modify balance directly. The only way to interact with it is through the methods, which enforce rules like "you can't withdraw more than you have."

This is the module pattern — using a closure to bundle private state with public methods. Before JavaScript had classes (and even now in many cases), this is how data encapsulation was achieved.

Interactive animation: The wall as scope boxes - the methods reach balance in one hop, outside code has no route.

Why Private State Matters

Public state — variables anyone can read or write — is the source of countless bugs in large programs. Someone modifies a value at the wrong time. Two parts of the code disagree about what state they share. Code paths interact in unexpected ways because they all touch the same global variables.

Private state solves this by giving you a single boundary through which all interactions happen. The state itself is invisible. The only way to interact with it is through the methods you provide. You control:

This is the same idea object-oriented programming has been using for decades. Classes use the private keyword to enforce it. JavaScript, until recently, didn't have a private keyword — closures were the only way to achieve true privacy. Even now, closures remain the most reliable mechanism.

Interactive animation: The underscore convention - a _balance property signals privacy, but outside code can still overwrite it.

The Counter Pattern, Generalized

The simplest module-pattern example is a counter — a small encapsulated thing that holds one number and exposes operations on it.

function createCounter(start = 0, step = 1) {
  let count = start;

  return {
    increment: () => {
      count += step;
      return count;
    },
    decrement: () => {
      count -= step;
      return count;
    },
    reset: () => {
      count = start;
      return count;
    },
    getValue: () => count
  };
}

const counter1 = createCounter();             // starts at 0, steps by 1
const counter2 = createCounter(100, 5);       // starts at 100, steps by 5

console.log(counter1.increment());   // 1
console.log(counter1.increment());   // 2

console.log(counter2.increment());   // 105
console.log(counter2.increment());   // 110

console.log(counter1.getValue());    // 2 — counter1 is unaffected
counter2.reset();
console.log(counter2.getValue());    // 100 — back to start

Each call to createCounter produces a fresh, independent counter. They share no state — counter1 and counter2 each have their own private count. This is closure isolation: each closure has its own captured scope.

The signature also uses default parameters from Module 5 — start = 0 and step = 1 let the caller skip arguments for the simple case.

The Module Pattern in Real Code

The module pattern is everywhere in older JavaScript and still common today. Here's a more realistic example — a small event emitter:

function createEventEmitter() {
  const listeners = {};   // private map of event name → array of callbacks

  return {
    on: (eventName, callback) => {
      if (!listeners[eventName]) {
        listeners[eventName] = [];
      }
      listeners[eventName].push(callback);
    },

    emit: (eventName, data) => {
      if (listeners[eventName]) {
        listeners[eventName].forEach(callback => callback(data));
      }
    },

    off: (eventName, callback) => {
      if (listeners[eventName]) {
        listeners[eventName] = listeners[eventName].filter(c => c !== callback);
      }
    }
  };
}

const emitter = createEventEmitter();

const greetingHandler = data => console.log(`Greeting received: ${data}`);
emitter.on("greeting", greetingHandler);

emitter.emit("greeting", "Hello!");      // logs: Greeting received: Hello!
emitter.emit("other", "ignored");        // no listener registered for "other"
emitter.off("greeting", greetingHandler);
emitter.emit("greeting", "Hello again"); // nothing — listener was removed

listeners is a private map that no outside code can access. The on, emit, and off methods are the only way to interact with it. All three close over the same listeners variable, so changes through one are visible to the others.

This pattern shows up in libraries, frameworks, and application code constantly. Every time you see a "factory function" that returns an object full of methods sharing state, closures are doing the work underneath.

Closures vs Classes

In modern JavaScript, classes (Module 8) can do similar things — encapsulating state and exposing methods. The difference:

// Module pattern with closure
function makeAccountClosure(initial) {
  let balance = initial;
  return {
    deposit: amount => balance += amount,
    getBalance: () => balance
  };
}

// Class
class AccountClass {
  #balance;   // private field (modern JavaScript)

  constructor(initial) {
    this.#balance = initial;
  }

  deposit(amount) {
    this.#balance += amount;
  }

  getBalance() {
    return this.#balance;
  }
}

const acc1 = makeAccountClosure(100);
const acc2 = new AccountClass(100);

acc1.deposit(50);
acc2.deposit(50);

console.log(acc1.getBalance());
console.log(acc2.getBalance());

Both achieve privacy. The closure version is older — it works in all JavaScript versions, including the oldest browsers. The class version uses #balance (private field syntax, fairly recent) for true privacy. Before private fields existed, classes used naming conventions like _balance to signal privacy without enforcing it.

Which to use is partly a style decision. Some codebases use classes for objects with state and behavior. Some prefer factory functions returning objects (closure-based). Modern frameworks like React lean toward the closure style for hooks. Both are valid. Both rely on the same scope mechanics underneath — the closure version uses it explicitly, the class version uses syntax that wraps it.

Interactive animation: Closure privacy vs #private fields - two real walls, compared side by side.

The Revealing Module Pattern

A variation of the module pattern, sometimes useful for clarity:

function createUserManager() {
  // Private state
  const users = [];

  // Private helper (not exposed)
  function findUserIndex(id) {
    return users.findIndex(u => u.id === id);
  }

  // Public methods, defined as named functions
  function addUser(user) {
    users.push(user);
  }

  function removeUser(id) {
    const index = findUserIndex(id);
    if (index >= 0) {
      users.splice(index, 1);
    }
  }

  function getUser(id) {
    const index = findUserIndex(id);
    return index >= 0 ? users[index] : null;
  }

  // Return only what should be public
  return {
    addUser,
    removeUser,
    getUser
  };
}

const manager = createUserManager();
manager.addUser({ id: 1, name: "Olena" });
manager.addUser({ id: 2, name: "Nikhil" });

console.log(manager.getUser(1));
manager.removeUser(1);
console.log(manager.getUser(1));

This style — define all the functions inside, then return an object that exposes only the public ones — is sometimes called the revealing module pattern. It's a stylistic choice. The result is the same: private state and private helpers, accessible only through the public methods.

graph TD
    A[createUserManager runs] --> B[Private: users array<br/>findUserIndex helper]
    A --> C[Public: addUser, removeUser, getUser<br/>all close over users + findUserIndex]
    A --> D[Returns object with<br/>public methods only]

    style B fill:#dc2626,color:#fff
    style C fill:#10b981,color:#fff
This diagram shows how the revealing module pattern works. When createUserManager runs, it creates private state (the users array) and a private helper function (findUserIndex). It also defines public methods — addUser, removeUser, getUser — all of which close over the private state. Finally, it returns an object containing only the public methods, hiding the private elements from outside code.

Common Pitfalls

Warning: The module pattern traps.

Returning the same shared state by accident. If createCounter had used a module-level count instead of a local one, all counters would share state. The "fresh state per call" is enabled by declaring variables inside the factory function. Move them outside and you lose the isolation.

Methods losing this context. When methods are stored in an object and passed around (e.g., as event handlers), their this can get lost. The closure pattern actually avoids this problem because the methods reference balance (or whatever) by closure, not by this. Module 8 covers this.

Memory growth from too many closures. Each closure holds onto its captured scope. Creating thousands of closures with large captured environments can cause significant memory use. Topic 6.6 covers memory implications.

Why This Matters in Interviews

Interview: "Implement a counter that can be incremented" or "create a function that returns an object with methods sharing private state" — these are common interview problems that test whether you can use closures deliberately, not just recognize them.

The pattern of "factory function returning an object" is one of the most useful patterns in JavaScript. Demonstrating fluency with it signals senior-level JS understanding.

Topic 6.7 — Interview Lens: Closures — covers the deeper treatment, including the specific implementation challenges interviewers ask.

Try It

Exercise: Implement a createStack factory function that returns an object representing a stack data structure. It should have:

  1. A private array to hold the items
  2. A push(item) method that adds an item
  3. A pop() method that removes and returns the top item
  4. A peek() method that returns the top item without removing it
  5. A size() method that returns the number of items
  6. An isEmpty() method

The internal array should not be accessible from outside — only through the methods.

function createStack() {
  // your code here
}

const stack = createStack();
stack.push(1);
stack.push(2);
stack.push(3);
console.log(stack.size());     // 3
console.log(stack.peek());     // 3
console.log(stack.pop());      // 3
console.log(stack.size());     // 2
console.log(stack.isEmpty());  // false

When you're done, verify there's no way to access the internal array from outside the returned object. That's the privacy guarantee closures give you.

Back to JavaScript Basic to Advance