Learn JS Series (#25) - IIFEs and the Module Pattern (the Pre-2015 Way to Get Privacy)

Published on HivePostify by @scipio · Mon Sep 07 2026

Learn JS Series (#25) - IIFEs and the Module Pattern (the Pre-2015 Way to Get Privacy)

What will I learn - You will learn what an IIFE is, and the syntax trick that makes it work; - why IIFEs were essential before JavaScript had block scope and real modules; - how the module pattern uses an IIFE plus closures to expose a public API while hiding internals; - the revealing module pattern variant, and how libraries used it; - why modern modules (Phase 8) replaced this, and why it is still worth understanding.

Requirements - A working modern computer running macOS, Windows or Ubuntu; - An installed Node.js (20+) distribution, or just a modern browser console; - Episodes 1-24 read, especially closures and scope.

Difficulty - Intermediate

Curriculum (of the Learn JS Series): - [Learn JS Series (#1) - What Is JavaScript, Why It Runs Everywhere, and How to Run It](https://hive.blog/hive-196387/@scipio/learn-js-series-1-what-is-javascript-why-it-runs-everywhere-and-how-to-run-it) - [Learn JS Series (#2) - Variables and Bindings](https://hive.blog/hive-196387/@scipio/learn-js-series-2-variables-and-bindings) - [Learn JS Series (#3) - The Primitive Types: number, string, boolean, null, undefined, symbol, bigint](https://hive.blog/hive-196387/@scipio/learn-js-series-3-the-primitive-types-number-string-boolean-null-undefined-symbol-bigint) - [Learn JS Series (#4) - Operators and Expressions: Arithmetic, Comparison, Logical, and Short-Circuiting](https://hive.blog/hive-196387/@scipio/learn-js-series-4-operators-and-expressions-arithmetic-comparison-logical-and-short-circuiting) - [Learn JS Series (#5) - Strings: Template Literals, Unicode, and the Methods You Actually Use](https://hive.blog/hive-196387/@scipio/learn-js-series-5-strings-template-literals-unicode-and-the-methods-you-actually-use) - [Learn JS Series (#6) - Numbers: IEEE 754, Why 0.1 + 0.2 Is Not 0.3, and How to Cope](https://hive.blog/hive-196387/@scipio/learn-js-series-6-numbers-ieee-754-why-01-02-is-not-03-and-how-to-cope) - [Learn JS Series (#7) - Control Flow: if/else, switch, and the Ternary Expression](https://hive.blog/hive-196387/@scipio/learn-js-series-7-control-flow-ifelse-switch-and-the-ternary-expression) - [Learn JS Series (#8) - Loops: for, while, for...of, for...in, and When to Use Which](https://hive.blog/hive-196387/@scipio/learn-js-series-8-loops-for-while-forof-forin-and-when-to-use-which) - [Learn JS Series (#9) - Functions: Declarations, Parameters, Return Values, and Hoisting](https://hive.blog/hive-196387/@scipio/learn-js-series-9-functions-declarations-parameters-return-values-and-hoisting) - [Learn JS Series (#10) - Scope and the Temporal Dead Zone: How JavaScript Finds Your Variables](https://hive.blog/hive-196387/@scipio/learn-js-series-10-scope-and-the-temporal-dead-zone-how-javascript-finds-your-variables) - [Learn JS Series (#11) - Arrays: The Workhorse Data Structure and Its Core Methods](https://hive.blog/hive-196387/@scipio/learn-js-series-11-arrays-the-workhorse-data-structure-and-its-core-methods) - [Learn JS Series (#12) - Objects: Key-Value Data, Dot vs Bracket Access, and Nesting](https://hive.blog/hive-196387/@scipio/learn-js-series-12-objects-key-value-data-dot-vs-bracket-access-and-nesting) - [Learn JS Series (#13) - Truthiness, Equality, and Coercion: == vs === Done Properly](https://hive.blog/hive-196387/@scipio/learn-js-series-13-truthiness-equality-and-coercion-vs-done-properly) - [Learn JS Series (#14) - Mini Project: A Command-Line Tip Calculator](https://hive.blog/hive-196387/@scipio/learn-js-series-14-mini-project-a-command-line-tip-calculator) - [Learn JS Series (#15) - First-Class Functions: Passing, Returning, and Storing Functions](https://hive.blog/hive-196387/@scipio/learn-js-series-15-first-class-functions-passing-returning-and-storing-functions) - [Learn JS Series (#16) - Arrow Functions vs function: Syntax, this, and When Each Wins](https://hive.blog/hive-196387/@scipio/learn-js-series-16-arrow-functions-vs-function-syntax-this-and-when-each-wins) - [Learn JS Series (#17) - Closures: The Single Most Important Idea in JavaScript](https://hive.blog/hive-196387/@scipio/learn-js-series-17-closures-the-single-most-important-idea-in-javascript) - [Learn JS Series (#18) - Higher-Order Functions: Functions That Take or Return Functions](https://hive.blog/hive-196387/@scipio/learn-js-series-18-higher-order-functions-functions-that-take-or-return-functions) - [Learn JS Series (#19) - Callbacks and the Callback Pattern (Before We Reach Promises)](https://hive.blog/hive-196387/@scipio/learn-js-series-19-callbacks-and-the-callback-pattern-before-we-reach-promises) - [Learn JS Series (#20) - Default, Rest, and Spread: Flexible Function Signatures](https://hive.blog/hive-196387/@scipio/learn-js-series-20-default-rest-and-spread-flexible-function-signatures) - [Learn JS Series (#21) - Destructuring Parameters: Named Arguments the JS Way](https://hive.blog/hive-196387/@scipio/learn-js-series-21-destructuring-parameters-named-arguments-the-js-way) - [Learn JS Series (#22) - The this Keyword: Five Rules That Explain Every Case](https://hive.blog/hive-196387/@scipio/learn-js-series-22-the-this-keyword-five-rules-that-explain-every-case) - [Learn JS Series (#23) - call, apply, and bind: Controlling this Explicitly](https://hive.blog/hive-196387/@scipio/learn-js-series-23-call-apply-and-bind-controlling-this-explicitly) - [Learn JS Series (#24) - Recursion: Base Cases, the Call Stack, and Stack Overflows](https://hive.blog/hive-196387/@scipio/learn-js-series-24-recursion-base-cases-the-call-stack-and-stack-overflows) - [Learn JS Series (#25) - IIFEs and the Module Pattern (the Pre-2015 Way to Get Privacy)](https://hive.blog/hive-196387/@scipio/learn-js-series-25-iifes-and-the-module-pattern-the-pre-2015-way-to-get-privacy) (this post)

Learn JS Series (#25) - IIFEs and the Module Pattern (the Pre-2015 Way to Get Privacy)

Solutions to Episode 24 Exercises

Exercise 1 - recursive power:

js function power(base, exponent) { if (exponent === 0) return 1; // base case: anything^0 is 1 return base power(base, exponent - 1); } console.log(power(2, 10)); // 1024

The insight: the base case exponent === 0 stops the recursion; each step peels off one multiplication until nothing is left to do.

Exercise 2 - recursive flatten:

js function flatten(arr) { let result = []; for (const item of arr) { if (Array.isArray(item)) result = result.concat(flatten(item)); else result.push(item); } return result; } console.log(flatten([1, [2, [3, 4]], 5])); // [1, 2, 3, 4, 5]

The insight: each nested array is handled by another call, so the function works no matter how deep the nesting goes.

Exercise 3 - a missing base case:

js function noBase(n) { return noBase(n + 1); // no base case -> RangeError: Maximum call stack size exceeded } function withBase(n) { if (n > 3) return "done"; // base case added return withBase(n + 1); } console.log(withBase(0)); // "done"

The insight: without a base case, each call stacks a frame until the stack fills and throws; the base case lets the stack stop growing and unwind cleanly.

With recursion behind us, we take a short trip into JavaScript history today. What follows is a piece of the language's past that is, strangely, still genuinely useful to understand - and it leans directly on the closures we spent episode 17 on. So this is not a museum tour; it is a working technique you will meet in real code.

The problem this solved

Cast your mind back to episode 2. Before 2015, JavaScript had no let, no const, and crucially no block scope - only var (which is function-scoped) and the single global scope. On top of that, it had no module system at all. Every script you loaded on a page shared one giant global namespace. If two scripts both declared a global count, they clobbered each other silently, and you got one of those bugs that eats an afternoon.

Think about what that meant in practice. You pull in a date library, a slider widget, and your own code, all via tags. Each one declares its helpers as plain globals. The slider has a helper, your code has a helper, and whichever loads last wins. There was no import, no file-level privacy, no namespace - just a shared bucket that every script scribbled into.

js // script A (some library) var util = "I am library A's util";

// script B (your code, loaded after A) var util = "mine now"; // silently overwrote A's util - good luck debugging that

Developers needed two things: a way to create a private scope so their internal variables would not leak, and a way to avoid polluting the global namespace with dozens of names. They found a clever solution using nothing but functions and closures. That solution is the IIFE, and it powered serious JavaScript for well over a decade.

The IIFE: an immediately invoked function expression

An IIFE (pronounced "iffy") is a function that you define and call in the same breath. The reasoning is simple once you connect two facts you already know. First, a function creates a brand new scope (episode 10). Second, variables declared inside that scope are invisible from the outside. So if you wrap some code in a function and run it immediately, you get a private scope that does its work and then vanishes, without leaving a named function lying around for anyone to call again:

js (function () { const secret = "hidden from the outside"; console.log(secret); // "hidden from the outside" })(); // console.log(secret); // ERROR (runtime): secret is not defined out here

The secret variable lives only inside the IIFE and never touches the global scope. Now look closely at the syntax, because this is the part that trips everyone up the first time. The whole function is wrapped in parentheses ( ... ), and then called with () at the end. Those wrapping parentheses are the entire trick.

Why are they needed? It comes down to how JavaScript decides whether it is reading a statement or an expression. If a line STARTS with the keyword function, the parser treats it as a function declaration - and a declaration is a definition, not something you can invoke inline. Trying to slap () after a declaration is a syntax error. But wrap the function in parentheses and you force the parser into expression mode: now function () { ... } is a value, a function expression, and a value can be immediately invoked with (). That is the whole story.

js // arrow IIFE (modern-looking, same idea): (() => { const temp = 42; console.log(temp); // 42, and temp stays private })();

You will see a couple of stylistic variants in the wild. Some people put the final call inside the outer parentheses, (function () { ... }()), which works identically. Others prefix the function with a ! or + to force expression mode instead of wrapping, like !function () { ... }(). They all achieve the same thing: turn a declaration into an expression so it can run on the spot. The wrapped-in-parens form is by far the most common and the clearest, so that is the one to reach for.

Crucially, an IIFE can take arguments and return a value, which is what makes it more than a curiosity. You can feed it the pieces it needs and capture whatever it produces:

js const result = (function (a, b) { const sum = a + b; // sum is private to this IIFE return sum 2; })(3, 4);

console.log(result); // 14 // console.log(sum); // ERROR (runtime): sum is not defined out here

A classic use, and one worth seeing because it shows the pain IIFEs relieved, was fixing the notorious var-in-a-loop bug. Because var is function-scoped, not block-scoped, every callback created in an old-style var loop closed over the SAME variable, so they all saw its final value. An IIFE per iteration gave each callback its own private copy:

js var callbacks = []; for (var i = 0; i 0; }

// PUBLIC: only what we return is accessible return { deposit(amount) { if (validate(amount)) balance += amount; return balance; }, getBalance() { return balance; }, }; })();

console.log(bank.deposit(100)); // 100 console.log(bank.getBalance()); // 100 // bank.balance is undefined; validate is invisible console.log(bank.balance); // undefined - truly private

Study what just happened. balance and validate are completely hidden - there is no way to reach them from outside, no bank.balance, no bank.validate. Only deposit and getBalance are public, and they can touch the private members purely through closure. This is exactly the encapsulation we built with factory functions back in episode 17, but applied at the whole-file level to create a self-contained, single-instance "module".

The distinction from a factory function is worth stating plainly. A factory (episode 17) is a function you call many times to get many independent objects. The module pattern is an IIFE you run ONCE to get a single, permanent object - a singleton. If your bank example had been a factory, you would write createBank() and call it repeatedly for separate accounts. As a module, there is exactly one bank, created the moment the script loads. Before real modules existed, this is precisely how every serious library protected its internals and offered a clean public surface.

The revealing module pattern

A popular refinement is the revealing module pattern. Instead of defining your public methods inline inside the returned object, you define ALL functions privately first, then return an object that simply maps public names to those private functions. The payoff is readability: the public API lives in one clear block at the bottom, so a reader can see the whole surface at a glance:

js const calculator = (function () { let result = 0;

function add(n) { result += n; return result; } function subtract(n) { result -= n; return result; } function reset() { result = 0; return result; } function value() { return result; }

// "reveal" the public API in one clear block return { add, subtract, reset, value }; })();

console.log(calculator.add(10)); // 10 console.log(calculator.subtract(3)); // 7 console.log(calculator.value()); // 7

At a glance, that bottom line return { add, subtract, reset, value } tells you exactly what is public. Everything else, including the result state, is private. Want to make a function private instead of public? Just leave it out of the returned object - no other change needed. This one-place-to-see-the-API property is why jQuery, and countless libraries of that era, were structured this way. It reads almost like a table of contents for the module.

There is one sharp edge worth knowing (it will save you a debugging session one day). Because the revealed object holds references to the functions, if a public method calls another one, it should call it by its private name, not through the returned object, or overrides can behave surprisingly. For everyday use this rarely bites, but it is the kind of subtle thing that separates "I copied a pattern" from "I understand the pattern".

Namespacing to avoid global pollution

The other big job IIFEs did was namespacing: attaching a single, controlled name to the global object rather than scattering many. A library would create one global namespace object and hang everything off it, minimizing the risk of clashing with other scripts on the page:

js const MyLib = (function () { function greet(name) { return Hello, ${name}; } function version() { return "1.0.0"; } return { greet, version }; })();

Tags: #stem#stemsocial#steemstem#javascript#programming

View full post on HivePostify →

Join HivePostify — Pakistan's First Web3 Platform →