JavaScriptDesign PatternsMedium

Closures for creating private variables

01

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.

02

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.

private-counter.tstypescript
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.

03

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.

module-pattern.tstypescript
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.

04

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 privacyClass #private fields
MechanismLexical scoping (variable not on any object)Hard private field (syntax-enforced)
Accessible via reflection?NoNo
MemoryEach instance gets own copy of methodsMethods shared on prototype
InheritanceNot applicable (composition-based)Subclasses can't access parent's #fields
TypeScript supportWorks naturallyCompiles to WeakMap (pre-ES2022) or native
DebuggingVariables visible in closure scope (DevTools)Visible as #field in DevTools
class-private-comparison.tstypescript
// 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.

05

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
06

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