Module 3 · Functions & Scope Mastery ⏱ 18 min

IIFE & The Module Pattern

By the end of this lesson you will be able to:
  • Encapsulate code with an Immediately Invoked Function Expression
  • Avoid global namespace pollution by hiding variables in a private scope
  • Return a public API while keeping implementation details private
  • Tell a singleton module (an IIFE) apart from a factory (a function that builds modules)

Before modules existed, every var you declared at the top level landed in one shared bucket: the global scope. Two scripts on the same page could each declare var data = [], and whichever loaded second silently overwrote the first. No error, no warning — just one script's data gone. That is global namespace pollution, and it is the problem this whole pattern was invented to solve.

The fix is to wrap your code in a function, because a function creates a private scope. An IIFE — Immediately Invoked Function Expression — does both jobs in one move: it defines a function and calls it right away, so its variables live for the run and then vanish, never touching the global bucket.

flowchart TD
  G["global scope"] --> I["(function(){...})()"]
  I --> P["private variables"]
  P --> X["destroyed after run"]
  style I fill:#e0900b,color:#fff
  style P fill:#e0900b,color:#fff
An IIFE creates a private scope that protects variables from the global scope.

The two pairs of parentheses

Read an IIFE from the outside in. There are two sets of parentheses, and each has a job:

(function () {
  const secret = 42;
})();

The first pair, wrapping function () { ... }, turns the function declaration into an expression — a value JavaScript can work with. The second pair, the bare () at the very end, is what actually calls it. Drop either one and the trick falls apart. The classic mistake is forgetting that final (): the function is then defined but never run, and the variable you thought held your result actually holds the function itself.

Run once, return a value, keep the internals private. Press Run.
const result = (function () {
  const x = 10;          // private — never reaches the global scope
  return x * 2;
})();
console.log(result);          // 20
console.log(typeof x);        // "undefined" — x stayed inside the IIFE

The module pattern: a public API over private state

An IIFE that merely runs and disappears is useful for staying tidy, but the real power comes when it returns something. Return an object whose methods close over private variables, and you have built a module: the outside world gets a small public API, while the data that API depends on stays locked inside.

const counter = (function () {
  let count = 0;            // private — no code outside can read or set it directly
  return {
    up() { count += 1; return count; },
    get() { return count; }
  };
})();

count is unreachable from the outside: counter.count is undefined, not zero. The only way to touch it is through up and get, the methods the module chose to expose. That is encapsulation — the same idea behind classes, built from nothing but a function and a closure.

flowchart TD
  I["(function(){ let count; return {up, get}; })()"] --> R["returns public API"]
  R --> PUB["{ up, get }"]
  I -.->|"count stays inside"| PRV["private — unreachable"]
  style PUB fill:#1e7f3d,color:#fff
  style PRV fill:#b91c1c,color:#fff
The IIFE returns a public API; the state it depends on stays sealed inside.
The module's methods work; the private variable does not exist outside.
const counter = (function () {
  let count = 0;
  return {
    up() { count += 1; return count; },
    get() { return count; }
  };
})();
console.log(counter.up());     // 1
console.log(counter.up());     // 2
console.log(counter.get());    // 2
console.log(counter.count);    // undefined — private

The classic trap: forgetting to invoke

Because an IIFE looks almost like an ordinary function definition, the single most common mistake is leaving off the final (). The code parses, runs without error, and quietly does nothing — your module was never created. The tell-tale sign is that the variable holding it is a function instead of your intended object.

A second, subtler point: an IIFE produces a singleton. It runs exactly once, so counter is one shared instance. If you need many independent counters, do not reach for another IIFE — write an ordinary function that returns a module, and call it once per counter. That factory shape is the transfer exercise below.

Missing the final () : the function is defined but never runs.
const maybe = (function () { return { hi: "hello" }; });   // no () — never invoked
console.log(typeof maybe);          // "function", not "object"

const real = (function () { return { hi: "hello" }; })();
console.log(typeof real);           // "object"
console.log(real.hi);               // "hello"
flowchart LR
  S["const m = (IIFE)()"] --> O1["ONE shared instance"]
  F["function make(){ return {...} }"] --> O2["call N times = N independent instances"]
  style O1 fill:#b45309,color:#fff
  style O2 fill:#3776ab,color:#fff
An IIFE runs once = one shared instance; a factory function runs per call = many.
Exercise

What does this print? The IIFE runs immediately and returns a value.

const result = (function () {
  const x = 10;
  return x * 2;
})();
console.log(result);
Exercise

Write createModule() as an IIFE that returns an object with get() and increment() methods. get returns the current value; increment adds 1 to it. The internal value must start at 0 and stay private.

const createModule = (function () {
  // return an object with get() and increment()
})();
Exercise

This module is meant to run immediately and expose a next() counter. But the second pair of parentheses is missing, so it only defines a function and never calls it — counter ends up holding the function itself rather than the module object. Add the one thing that actually invokes the IIFE.

const counter = (function () {
  let n = 0;
  return { next() { n += 1; return n; } };
});
Exercise

What does this print (two lines)? count is private — there is no property to read it from outside, only get().

const mod = (function () {
  let count = 5;
  return { get() { return count; } };
})();
console.log(mod.get());
console.log(mod.count);
Exercise

An IIFE is a singleton: one shared instance. Write makeTally() as a factory — an ordinary function that RETURNS a fresh, independent tally each time it is called. Each tally has bump() (add 1 and return the new total) and value() (return the current total). The total starts at 0 and must stay private, and two tallies must not share state. This is the transfer task: the lesson's module is a single IIFE; you must produce many independent ones.

function makeTally() {
  // return a fresh { bump, value } with its own private total
}

Recap

  • An IIFE(function () { ... })() — defines and runs a function in one move, so its variables never leak into the global scope.
  • Two parenthesis pairs: the first makes it an expression, the second invokes it. Forgetting the final () is the classic trap — the module never runs.
  • The module pattern is an IIFE that returns an object: a public API over private state, built from a closure.
  • Private variables are unreachable from outside — mod.count is undefined, not the value.
  • An IIFE is a singleton (one instance); for many independent instances, write a factory function that returns a module per call.
  • Modern code often uses ES import/export instead, but IIFEs still appear in legacy code, build output, and interviews.

Next you'll step back to the workhorses of everyday iteration — the loop patterns that pair naturally with all of this.

Checkpoint quiz

What does IIFE stand for?

What happens if you write (function () { return { a: 1 }; }); and assign it — with no final ()?

Why is the module pattern useful?

Go deeper — technical resources