Module and Revealing Module

A cart total that read $0.00 at checkout, but only sometimes, and only in production. That was the bug. It passed every test, worked on my machine, worked in staging. In production maybe one page load in fifty came back with a total of zero, and the order went through for free.

The page loaded about a dozen script tags, the way pages did then. Two of them declared var total at the top level. The cart used total for the running sum. A marketing snippet loaded a promo countdown widget that also kept a global total, its own unrelated number, and reset it to zero when the timer expired. Same name, same global object, and the global object does not care what you meant by it. Whoever assigns last wins. On the unlucky page loads where the promo widget ran after the cart, the cart’s total quietly became the promo’s total.

No module system, no imports, no privacy. Every script shared one namespace, and a shared namespace with no walls is a bug waiting for a second author. The fix the whole industry reached for, years before the language grew up, is the thing this article is about. It is built on one feature you already have: the closure.

One function, one private room

A function gives you a scope. Variables declared inside it belong to that call and cannot be named from outside. So wrap the feature in a function, keep the state in there, and hand back only what callers are allowed to touch.

const cart = (function () {
  let items = [];
  let total = 0;

  function add(item) {
    items.push(item);
    total += item.price;
  }

  function summary() {
    return { count: items.length, total };
  }

  return { add, summary };
})();

items and total live inside a function call that has already finished running. Nothing outside can reach them, because nothing outside can name them. The cart binding holds a plain object with two methods and no data. The promo widget can declare and clobber its own global total until the end of time, and this cart never feels it, because this cart has no total anywhere a stranger could find.

That wrapper, (function () { ... })(), has a name: an Immediately Invoked Function Expression, an IIFE. You define a function and call it on the spot. The reason is narrow and worth stating plainly. You need a function to get a private scope, but you do not want to keep the function around or call it later. You want it to run once, right now, build its private state, hand back a public surface, and vanish. The parentheses around the function turn a declaration into an expression so you are allowed to call it inline, and the trailing () runs it.

const counter = (function () {private scope, unreachable from outsidelet count = 0;const log = [];function tick() { count++; }return {increment: tick,value: () => count})();outside codecounter.increment() okcounter.value() okcounter.count => undefinedcounter.log => undefinedthe private names are simply not there
The IIFE runs once, keeps its variables private, and returns a small object. Outside code can only touch what came back.

That green arrow is the whole idea. The returned object is the only thing that crosses the boundary. Everything else stays sealed in a scope that finished executing and that no name outside the function points into. This is encapsulation, in a language that in 1995 shipped with no keyword for it.

Keep it off the global object

The cart above is still a global if you write it at the top of a script. One global instead of five is already a large win, but the era’s codebases went further and hung everything off a single namespace object so the global scope stayed nearly empty.

window.App = window.App || {};

App.cart = (function () {
  let items = [];
  let total = 0;

  function add(item) { items.push(item); total += item.price; }
  function summary() { return { count: items.length, total }; }

  return { add, summary };
})();

window.App = window.App || {} is the namespace pattern in one line: make the namespace if it does not exist yet, reuse it if some other script already made it. Every feature adds exactly one property to App. The global object goes from a junk drawer of fifty loose names, any of which two authors might pick by accident, to a single App with a tidy tree hanging off it. Collisions stop being a coin flip.

before · one shared global, no wallswindowitemstotal (cart)addtotal (promo)summarypromoTimeruserdiscounttwo scripts, one name total,last write silently winsanyone can read or overwriteany of these at any timeafter · one namespace, private interiorwindowAppApp.cartpublic surfaceadd() summary()private, sealed in the closurelet items = [];let total = 0;no outside path in
Left: everything on the global object, including two different total variables that overwrite each other. Right: one namespace, and the state sealed inside where no one can clobber it.

The revealing module: name the API in one place

The plain module builds its return object inline, which reads fine for two methods. Give a module ten private helpers and five public ones and that inline return { ... } gets noisy, and worse, you cannot tell at a glance which functions are public without reading the whole body and cross-checking.

The revealing module flips the order. Define everything as ordinary private functions and variables. Then, at the very bottom, return an object that maps public names to those private ones. The last few lines become a table of contents for the module’s surface.

const store = (function () {
  let state = { user: null, cart: [] };

  function setUser(user) { state.user = user; }
  function addToCart(item) { state.cart.push(item); }
  function clearCart() { state.cart = []; }
  function itemCount() { return state.cart.length; }
  function serialize() { return JSON.stringify(state); }

  // one place that says exactly what is public
  return {
    setUser,
    addToCart,
    itemCount,
  };
})();

Read the bottom three lines and you know the entire public API: setUser, addToCart, itemCount. clearCart, serialize, and state are private, on purpose, and you learned that without scanning a single function body. That is the whole reason the variant exists. Everything is written as a normal declaration, and the return is a curated index you control by hand.

const store = (function () {let state = { … }function setUser()function addToCart()function clearCart()function itemCount()function serialize()grey = kept private · blue = revealed belowreturn {setUser,addToCart,itemCount,};
Everything is defined privately. The return object at the side is the only place that names the public API, so the surface is readable in one glance.

There is one sharp edge worth knowing, because it has genuinely confused people in tests.

A module is a singleton, and usually that is fine

Look again at that IIFE. It runs exactly once. There is one store, created the instant the script is parsed, shared by everyone who references it. That is the Singleton pattern, achieved with zero ceremony. No class, no getInstance(), no static field, no guard against a second instance. You could not make a second one by accident if you tried, because there is no constructor to call twice.

That is often exactly what you want, and sometimes exactly what you do not. Shared config or a single connection pool being one instance is correct and convenient. Shared mutable business state that every part of the app (and every test) silently inherits is how you get flaky tests and spooky action at a distance. The singleton pattern has its own article on where the line sits. For now, just notice that the module pattern hands you a singleton whether or not you asked for one.

You rarely hand-roll this anymore

Everything above was state of the art when a script tag was your only unit of code. Then the language grew up, and two features took over most of the job the IIFE was faking.

ES modules give you the private scope for free. A module file is its own scope. Anything you do not export is private to that file, permanently, enforced by the language rather than by a closure trick. The IIFE wrapper becomes pure redundancy, because the file already is the wrapper. (Modules and packages covers the loading and bundling side.)

// cart.js, the file itself is the private scope
let items = [];
let total = 0;

export function add(item) {
  items.push(item);
  total += item.price;
}

export function summary() {
  return { count: items.length, total };
}

items and total are unreachable from any other file. There is no window.App, no IIFE, no return map. Write import { add } from './cart.js' and you get precisely the surface the file chose to export, nothing more. And like the IIFE, a module is evaluated once and its bindings are shared, so the singleton nature rides along without any extra code.

Classes give real privacy when you need many instances. When you genuinely want more than one object, each with its own private state, a class with #-prefixed fields gives you hard privacy that the old _underscore convention never did. (The syntax details live in classes and static blocks.)

class Counter {
  #count = 0;

  increment() { this.#count++; }
  get value() { return this.#count; }
}

const c = new Counter();
c.increment();
c.value;      // 1
c.#count;     // SyntaxError, caught before the code even runs

#count is not a convention you politely ask other people to respect. It is enforced by the parser. Reaching for c.#count from outside the class is a syntax error, not a value of undefined, which means it is caught before the program runs at all. That is the true privacy the module pattern spent fifteen years approximating with scope tricks, now built into the language. (You can even brand-check with #count in obj, which safely tells you whether an object is a real Counter without a try/catch.)

Same wall, three eras of drawing itIIFE modulearound 2006private (in scope)let count = 0;function tick()public surfacereturn { increment }privacy by closureES module2015private (file scope)let total = 0;// not exportedpublic surfaceexport function add()privacy by the fileclass #private2022private (enforced)#count = 0;outside = SyntaxErrorpublic surfaceincrement()privacy by the languagethe syntax changes; hide the state, expose a small surface stays constant
The same idea, hide the state and expose a small surface, drawn three ways across three eras. The ceremony shrinks left to right; the idea does not move.

What actually survived

So is the module pattern dead? The IIFE ceremony, mostly yes. If you are writing (function(){ ... })() in 2026 to get privacy in code that runs through a bundler, you are rebuilding a wall the file already gives you. The idea, though, is not dead in the slightest. Encapsulation plus a minimal public surface is something you apply constantly, usually without naming it.

The clearest living form is the factory function. A function that builds private state in a closure and returns an object of methods over it is the module pattern, just parameterized so you can make more than one.

function createRateLimiter(maxPerMinute) {
  const hits = [];                       // private, one array per limiter

  return {
    allow() {
      const now = Date.now();
      while (hits.length && hits[0] < now - 60_000) hits.shift();
      if (hits.length >= maxPerMinute) return false;
      hits.push(now);
      return true;
    },
  };
}

const api = createRateLimiter(100);
const login = createRateLimiter(5);

api.allow();     // true, and nothing anywhere can reach `hits`

Same shape as the IIFE module: private state, a returned surface, no door in from outside. The only difference is that a factory takes an argument and can be called many times, so api and login are two completely independent limiters with two separate hits arrays. The old module was this exact code with the call baked in and the argument list empty. That is the module pattern still earning its keep, and you will write it often.

When a closure module still beats a class

Given classes now have real #private, when do you reach for the closure form instead? A few honest cases.

No this, no new. A closure captures variables lexically, so there is no this to lose when a method gets passed around. const { allow } = createRateLimiter(100) and allow works on its own, detached, forever. Pull a method off a class instance the same way and this is gone unless you bound it first. For anything you hand to setTimeout, an event listener, or array.map, that difference is real ergonomics, not theory.

Simple private state, no hierarchy. If you are not extending anything and not building a family of types, a factory returning an object is less machinery than a class for an identical result. No constructor, no new, fewer moving parts for the next reader to hold in their head.

Private by construction, not by rule. A closure variable is not a hard-to-reach property. It is not a property of anything at all, so it cannot be enumerated, JSON.stringify-ed by accident, or dug out by reflection. #private fields get very close to this, and they are the right tool most of the time. The closure has simply been airtight since the language shipped.

And the class earns its place the moment you need many instances where instanceof matters, or shared methods living once on the prototype instead of a fresh copy of each function per object, or an inheritance relationship you actually have (which, before you commit to it, is worth weighing against composition).

See the wall hold

Two counters below. The one on the left is built with a closure that exposes increment and get but never the raw count. The one on the right is a naive object with a public count field. Increment each, then hit the tamper button, which reaches in and tries to set count = 999 from outside. Watch which one lies to you.

interactiveTry to corrupt the count from outside

The tamper button runs the same line against both, x.count = 999. On the naive object it lands, because count is a real, writable, public property. On the closure counter it sets a meaningless count property on the returned wrapper object, but get() never reads that property. It reads the variable sealed inside makeCounter, which no assignment from outside can name. That gap is the entire value of the pattern, and it is the difference between a checkout that charges the right amount and one that occasionally charges nothing.

Summary

  • Before JavaScript had modules, the module pattern faked privacy with a closure: a function keeps its variables in scope and returns only a small public object, so internal state cannot be reached or corrupted from outside.
  • The IIFE, (function(){ ... })(), runs that function once to build the private state and hand back the surface, then gets out of the way. It is the historical form, not something to reach for in new bundled code.
  • The namespace pattern (window.App = window.App || {}) kept the global scope nearly empty by hanging every feature off one object, which is how teams survived a world with no imports.
  • The revealing module defines everything privately and returns a map of public names at the bottom, so the API is legible in one place. Its catch: the revealed references are a snapshot, and internal calls bypass them, which can surprise you in tests.
  • A module instance is a singleton for free, because the IIFE (and an ES module) is evaluated once and shared. Convenient for config and pools, a trap for shared mutable state. See singleton.
  • Modern JavaScript mostly retired the hand-rolled version. ES modules make anything not exported private to the file, and classes have real #private fields enforced by the parser. You rarely write an IIFE for privacy anymore.
  • The principle survived intact. A factory that closes over private state and returns methods is the module pattern, parameterized to make many instances. You write it all the time.
  • When to use a closure module: simple private state, no this and no new, detachable methods, no type hierarchy. When to avoid it: you need many instances with instanceof and shared prototype methods, or a real inheritance relationship, where a class with #private fields fits better.