Closures for creating private variables
The Short Answer
Closures let you create truly private variables in JavaScript — variables that are completely inaccessible from outside a function, with no way to read or modify them except through the specific functions you expose. This is JavaScript's native encapsulation mechanism, predating classes and #private fields. The pattern works because inner functions retain access to their outer scope's variables even after the outer function returns, while nothing else can reach those variables.
The Basic Pattern
The core idea: define variables inside a function, then return an object (or function) that has access to those variables through closure. The returned interface is the only way to interact with the private state — there's no obj.privateVar to access directly.
function createCounter(initial = 0) {
let count = initial; // Private — only accessible through returned methods
return {
increment() {
count += 1;
return count;
},
decrement() {
count -= 1;
return count;
},
getCount() {
return count;
},
reset() {
count = initial;
return count;
},
};
}
const counter = createCounter(10);
counter.increment(); // 11
counter.increment(); // 12
counter.decrement(); // 11
counter.getCount(); // 11
counter.reset(); // 10
// count is truly private — no way to access it directly
console.log((counter as any).count); // undefined
console.log(Object.keys(counter)); // ['increment', 'decrement', 'getCount', 'reset']
Unlike class private fields or underscore conventions, closure-based privacy is enforced by the language's scoping rules. There's no reflection, no Object.getOwnPropertyNames(), no way to reach count from outside. It simply doesn't exist on any accessible object.
The Module Pattern
The module pattern uses an IIFE (Immediately Invoked Function Expression) to create a closure that runs once and returns a public API. Private variables and helper functions live inside the IIFE's scope, invisible to the outside world. This was the standard way to create modules before ES modules existed.
const authModule = (() => {
// Private state — not accessible outside this IIFE
let currentUser: { id: string; name: string } | null = null;
let token: string | null = null;
const SESSION_DURATION = 3600000; // 1 hour
// Private helper — not exposed in the returned object
function isTokenExpired(): boolean {
// Internal logic
return false;
}
// Public API — the only way to interact with private state
return {
login(credentials: { email: string; password: string }) {
// Validate and set private state
token = 'jwt_token_here';
currentUser = { id: '1', name: 'Alice' };
return true;
},
logout() {
token = null;
currentUser = null;
},
getCurrentUser() {
if (isTokenExpired()) return null;
return currentUser ? { ...currentUser } : null; // Return copy, not reference
},
isAuthenticated() {
return token !== null && !isTokenExpired();
},
};
})();
authModule.login({ email: 'alice@example.com', password: '...' });
authModule.getCurrentUser(); // { id: '1', name: 'Alice' }
// authModule.token → undefined (private)
// authModule.currentUser → undefined (private)
// authModule.isTokenExpired → undefined (private helper)
The IIFE runs immediately and returns the public object. The private variables (currentUser, token, SESSION_DURATION) and private functions (isTokenExpired) are trapped in the closure — only the four public methods can access them.
Closure Privacy vs Class Private Fields
Modern JavaScript offers #private fields in classes. Both approaches achieve encapsulation, but they work differently and have different tradeoffs. Understanding both helps you choose the right tool.
| Closure privacy | Class #private fields | |
|---|---|---|
| Mechanism | Lexical scoping (variable not on any object) | Hard private field (syntax-enforced) |
| Accessible via reflection? | No | No |
| Memory | Each instance gets own copy of methods | Methods shared on prototype |
| Inheritance | Not applicable (composition-based) | Subclasses can't access parent's #fields |
| TypeScript support | Works naturally | Compiles to WeakMap (pre-ES2022) or native |
| Debugging | Variables visible in closure scope (DevTools) | Visible as #field in DevTools |
// Class with #private fields (ES2022+)
class Counter {
#count = 0; // Hard private — syntax error if accessed outside
increment() {
this.#count += 1;
return this.#count;
}
getCount() {
return this.#count;
}
}
const counter = new Counter();
counter.increment(); // 1
// counter.#count; // SyntaxError: Private field '#count' must be declared in an enclosing class
// Closure equivalent (from earlier)
function createCounter() {
let count = 0;
return {
increment() { return ++count; },
getCount() { return count; },
};
}
The closure version duplicates methods per instance (each object gets its own increment function). The class version shares methods on the prototype (more memory-efficient for many instances). For singletons or few instances, closures are fine. For many instances, classes with #private are more efficient.
Real-World Use Cases
Closure-based privacy shows up in many practical patterns beyond simple counters. Here are scenarios where it's the natural choice.
- Configuration with secrets
- API keys, tokens stored in closure — never exposed on any object
- Rate limiters
- Internal request count and timestamps hidden from consumers
- Caches with eviction logic
- Cache map and LRU tracking are implementation details
- State machines
- Current state and transition rules are private — only valid transitions exposed
- Event emitters
- Listener registry is private — only on/off/emit are public
Why Interviewers Ask This
This question tests whether you understand closures beyond the textbook definition — can you apply them to solve real design problems? Interviewers want to see that you know how to achieve encapsulation in JavaScript, understand the module pattern, can compare closure privacy with class private fields, and know the memory tradeoffs. It demonstrates both language mastery and software design thinking.
Quick Revision Cheat Sheet
Pattern: Variables in outer function → returned inner functions access them via closure
Module pattern: IIFE that returns public API, keeps state private in closure
True privacy: No reflection, no Object.keys, no way to access from outside
vs #private: Closures duplicate methods per instance; #private shares via prototype
Best for: Singletons, modules, factories with few instances
Return copies: Return { ...obj } not the reference — prevent external mutation