Free lesson · JavaScript Basic to Advance
Closures for Private State
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:
- What can be read. Maybe you provide a
getBalance()but not direct property access. - What can be modified. Maybe you allow
deposit()but only with positive amounts. - What rules apply. Maybe
withdraw()checks the balance before modifying.
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
createCounterhad used a module-levelcountinstead 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
thiscontext. When methods are stored in an object and passed around (e.g., as event handlers), theirthiscan get lost. The closure pattern actually avoids this problem because the methods referencebalance(or whatever) by closure, not bythis. 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
createStackfactory function that returns an object representing a stack data structure. It should have:
- A private array to hold the items
- A
push(item)method that adds an item- A
pop()method that removes and returns the top item- A
peek()method that returns the top item without removing it- A
size()method that returns the number of items- An
isEmpty()methodThe 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()); // falseWhen 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.