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
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.
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
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.
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
What does this print? The IIFE runs immediately and returns a value.
const result = (function () {
const x = 10;
return x * 2;
})();
console.log(result);
The function runs right away.
It returns 10 * 2, which is stored in result and logged.
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()
})();
Use let value = 0 inside the IIFE.
Return an object with two methods that close over value.
const createModule = (function () {
let value = 0;
return {
get() { return value; },
increment() { value += 1; }
};
})();
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; } };
});
Without the final (), the IIFE is defined but never runs, so counter is a function.
Append () right after the closing } of the function expression to invoke it immediately.
const counter = (function () {
let n = 0;
return { next() { n += 1; return n; } };
})();
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);
get() closes over count, so mod.get() returns 5.
count is a private closure variable, not a property — mod.count is undefined.
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
}
Declare let total = 0 inside makeTally, then return the object — each call gets its own total.
Two separate calls build two separate closures, so bumping one leaves the other at 0.
function makeTally() {
let total = 0;
return {
bump() { total += 1; return total; },
value() { return 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.countisundefined, 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/exportinstead, 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.