Learn how to organize JavaScript code with IIFEs, namespaces, and ES6 modules. Understand private scope, exports, dynamic imports, and common module mistakes.
How do you prevent your JavaScript variables from conflicting with code from other files or libraries? How do modern applications organize thousands of lines of code across multiple files?
Copy
Ask AI
// Modern JavaScript: Each file is its own module// utils.jsexport function formatDate(date) { return date.toLocaleDateString()}// main.jsimport { formatDate } from './utils.js'console.log(formatDate(new Date())) // "12/30/2025"
This is ES6 modules. It’s JavaScript’s built-in way to organize code into separate files, each with its own private scope. But before modules existed, developers invented clever patterns like IIFEs and namespaces to solve the same problems.
What you’ll learn in this guide:
What IIFEs are and why they were invented
How to create private variables and avoid global pollution
What namespaces are and how to use them
Modern ES6 modules: import, export, and organizing large projects
The evolution from IIFEs to modules and why it matters
Common mistakes with modules and how to avoid them
Prerequisite: This guide assumes you understand scope and closures. IIFEs and the module pattern rely on closures to create private variables. If closures feel unfamiliar, read that guide first!
An IIFE (Immediately Invoked Function Expression) is a JavaScript function that runs as soon as it’s defined. As documented on MDN, it creates a private scope to protect variables from polluting the global namespace. This pattern was essential before ES6 modules existed.
Copy
Ask AI
// An IIFE — runs immediately, no calling needed(function() { const private = "I'm hidden from the outside world"; console.log(private);})(); // Runs right away!// The variable "private" doesn't exist out here// console.log(private); // ReferenceError: private is not defined
The parentheses around the function turn it from a declaration into an expression, and the () at the end immediately invokes it. This was the go-to pattern for creating private scope before JavaScript had built-in modules. According to the 2023 State of JS survey, ES modules are now used by the vast majority of JavaScript developers, but IIFEs remain common in bundler output and legacy codebases.
Historical context: IIFEs were everywhere in JavaScript codebases from 2010-2015. Today, most projects use ES6 modules (import/export), so you won’t write many IIFEs in modern code. However, understanding them is valuable. You’ll encounter IIFEs in older codebases, libraries, and they’re still useful for specific cases like async initialization or quick scripts.
Imagine you’re working at a desk covered with papers, pens, sticky notes, and coffee cups. Everything is mixed together. When you need to find something specific, you have to dig through the mess. And if someone else uses your desk? Chaos.Now imagine organizing that desk:
Copy
Ask AI
┌─────────────────────────────────────────────────────────────────────┐│ THE MESSY DESK (No Organization) ││ ││ password = "123" userName = "Bob" calculate() ││ config = {} helpers = {} API_KEY = "secret" ││ utils = {} data = [] currentUser = null init() ││ ││ Everything is everywhere. Anyone can access anything. ││ Name conflicts are common. It's hard to find what you need. │└─────────────────────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────────────────────┐│ THE ORGANIZED DESK (With Modules) ││ ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││ │ auth.js │ │ api.js │ │ utils.js │ ││ │ │ │ │ │ │ ││ │ • login() │ │ • fetch() │ │ • format() │ ││ │ • logout() │ │ • post() │ │ • validate()│ ││ │ • user │ │ • API_KEY │ │ • helpers │ ││ └─────────────┘ └─────────────┘ └─────────────┘ ││ ││ Each drawer has its own space. Take only what you need. ││ Private things stay private. Everything is easy to find. │└─────────────────────────────────────────────────────────────────────┘
This is the story of how JavaScript developers learned to organize their code:
First, we had the messy desk — everything in the global scope
Then, we invented IIFEs — a clever trick to create private spaces
Next, we created Namespaces — grouping related things under one name
Finally, we got Modules — the modern, built-in solution
Let’s learn each approach and understand when to use them.
Function Expression — a function written as an expression (not a declaration)
Copy
Ask AI
// A normal function — you define it, then call it laterfunction greet() { console.log("Hello!");}greet(); // You have to call it// An IIFE — it runs immediately, no calling needed(function() { console.log("Hello!");})(); // Runs right away!
To understand IIFEs, you need to understand the difference between expressions and statements in JavaScript.
Copy
Ask AI
┌─────────────────────────────────────────────────────────────────────┐│ EXPRESSION vs STATEMENT ││ ││ EXPRESSION = produces a value ││ ───────────────────────────── ││ 5 + 3 → 8 ││ "hello" → "hello" ││ myFunction() → whatever the function returns ││ x > 10 → true or false ││ function() {} → a function value (when in expression position)││ ││ STATEMENT = performs an action (no value produced) ││ ────────────────────────────────────────────────── ││ if (x > 10) { } → controls flow, no value ││ for (let i...) { } → loops, no value ││ function foo() { } → declares a function, no value ││ let x = 5; → declares a variable, no value │└─────────────────────────────────────────────────────────────────────┘
The key insight: A function can be written two ways:
Copy
Ask AI
// FUNCTION DECLARATION (statement)// Starts with the word "function" at the beginning of a linefunction greet() { return "Hello!";}// FUNCTION EXPRESSION (expression)// The function is assigned to a variable or wrapped in parenthesesconst greet = function() { return "Hello!";};
// ✗ This FAILS — JavaScript sees "function" and expects a declarationfunction() { console.log("This causes a syntax error!");}(); // SyntaxError: Function statements require a function name // (exact error message varies by browser)// ✓ This WORKS — Parentheses make it an expression(function() { console.log("This works!");})();// The parentheses tell JavaScript: "This is a value, not a declaration"
(function() { // your code here})();// Let's label each part:( function() { ... } ) ();│ │ ││ │ └─── 3. Invoke (call) it immediately│ ││ └─────── 2. Wrap in parentheses (makes it an expression)│└──────────────────────────── 1. Define a function
Why the parentheses? Without them, JavaScript thinks you’re writing a function declaration, not an expression. The parentheses tell JavaScript: “This is a value (an expression), not a statement.”
Before ES6 modules, JavaScript had a big problem: everything was global. When scripts were loaded with regular <script> tags, variables declared with var outside of functions became global and were shared across all scripts on the page, leading to conflicts:
Copy
Ask AI
// file1.jsvar userName = "Alice"; // var creates global variablesvar count = 0;// file2.js (loaded after file1.js)var userName = "Bob"; // Oops! Overwrites the first userNamevar count = 100; // Oops! Overwrites the first count// Now file1.js's code is broken because its variables were replaced
IIFEs solved this by creating a private scope:
Copy
Ask AI
// file1.js — wrapped in an IIFE(function() { var userName = "Alice"; // Private to this IIFE var count = 0; // Private to this IIFE // Your code here...})();// file2.js — also wrapped in an IIFE(function() { var userName = "Bob"; // Different variable, no conflict! var count = 100; // Different variable, no conflict! // Your code here...})();
One of the most powerful uses of IIFEs is creating private variables that can’t be accessed from outside:
Copy
Ask AI
const counter = (function() { // Private variable — can't be accessed directly let count = 0; // let is block-scoped, perfect for private state // Private function — also hidden function log(message) { console.log(`[Counter] ${message}`); } // Return public interface return { increment() { count++; log(`Incremented to ${count}`); }, decrement() { count--; log(`Decremented to ${count}`); }, getCount() { return count; } };})();// Using the countercounter.increment(); // [Counter] Incremented to 1counter.increment(); // [Counter] Incremented to 2console.log(counter.getCount()); // 2// Trying to access private variablesconsole.log(counter.count); // undefined (it's private!)counter.log("test"); // TypeError: counter.log is not a function
This pattern is called the Module Pattern. It uses closures to keep variables private. It was the standard way to create “modules” before ES6.
// Passing jQuery to ensure $ refers to jQuery(function($) { // Inside here, $ is definitely jQuery $(".button").click(function() { console.log("Clicked!"); });})(jQuery);// Passing window and document for performance(function(window, document) { // Accessing window and document is slightly faster // because they're local variables now const body = document.body; const location = window.location;})(window, document);
// In a <script> tag (not a module)(function() { // All variables here are private const secretKey = "abc123"; // Only expose what's needed window.MyApp = { init() { /* ... */ } };})();
const MyApp = {};// Use IIFE to add features with private variablesMyApp.Counter = (function() { // Private let count = 0; // Public return { increment() { count++; }, decrement() { count--; }, getCount() { return count; } };})();MyApp.Logger = (function() { // Private const logs = []; // Public return { log(message) { logs.push({ message, time: new Date() }); console.log(message); }, getLogs() { return [...logs]; // Return a copy } };})();// UsageMyApp.Counter.increment();MyApp.Logger.log("Counter incremented");
Namespaces vs Modules: Namespaces are a pattern, not a language feature. They help organize code but don’t provide true encapsulation. Modern ES6 modules are the preferred approach for new projects, but you’ll still see namespaces in older codebases and some libraries.
Modules are JavaScript’s built-in way to organize code into separate files, each with its own scope. Unlike IIFEs and namespaces (which are patterns), modules are a language feature.The export statement makes functions, objects, or values available to other modules. The import statement brings them in.
Copy
Ask AI
// math.js — A module fileexport function add(a, b) { return a + b;}export function subtract(a, b) { return a - b;}export const PI = 3.14159;// main.js — Another module that uses math.jsimport { add, subtract, PI } from './math.js';console.log(add(2, 3)); // 5console.log(subtract(10, 4)); // 6console.log(PI); // 3.14159
// Option 1: Use .mjs extension// math.mjsexport function add(a, b) { return a + b; }// Option 2: Add "type": "module" to package.json// Then use .js extension normally
What about require() and module.exports? You might see this older syntax in Node.js code:
This is called CommonJS, Node.js’s original module system. While still widely used, ES modules (import/export) are the modern standard and work in both browsers and Node.js. New projects should use ES modules.
Named exports let you export multiple things from a module. Each has a name.
Copy
Ask AI
// utils.js// Export as you declareexport const PI = 3.14159;export function square(x) { return x * x;}export class Calculator { add(a, b) { return a + b; }}// Or export at the endconst E = 2.71828;function cube(x) { return x * x * x; }export { E, cube };
Each module can have ONE default export. It’s the “main” thing the module provides.
Copy
Ask AI
// greeting.js// Default export — no name needed when importingexport default function greet(name) { return `Hello, ${name}!`;}// You can have named exports tooexport const defaultName = "World";
Copy
Ask AI
// Another example — default exporting a class// User.jsexport default class User { constructor(name) { this.name = name; } greet() { return `Hi, I'm ${this.name}`; }}
// utils.jsexport function formatDate(date) { /* ... */ }export function formatCurrency(amount) { /* ... */ }export function formatPhone(number) { /* ... */ }// Import only what you needimport { formatDate } from './utils.js';
Use when:
The module has one main purpose
You’re exporting a class or component
The import name doesn’t need to match
Copy
Ask AI
// Button.js — React componentexport default function Button({ label }) { return <button>{label}</button>;}// Import with any nameimport MyButton from './Button.js';
Import specific things by name (must match the export names):
Copy
Ask AI
// Import specific itemsimport { PI, square } from './utils.js';// Import with a different name (alias)import { PI as pi, square as sq } from './utils.js';// Import everything as a namespace objectimport * as Utils from './utils.js';console.log(Utils.PI);console.log(Utils.square(4));
Import the default export with any name you choose:
Copy
Ask AI
// The name doesn't have to match the export nameimport greet from './greeting.js';// In a DIFFERENT file, you could use a different name:// import sayHello from './greeting.js'; // Same function, different name// import xyz from './greeting.js'; // Still the same function!// Combine default and named importsimport greet, { defaultName } from './greeting.js';
Why any name? Default exports don’t have a required name, so you choose what to call it when importing. This is useful but can make code harder to search. Named exports are often preferred for this reason.
Sometimes you just want to run a module’s code without importing anything:
Copy
Ask AI
// This runs the module but imports nothingimport './polyfills.js';import './analytics.js';// Useful for:// - Polyfills that add global features// - Initialization code// - CSS (with bundlers)
// Named importsimport { a, b, c } from './module.js';// Named import with aliasimport { reallyLongName as short } from './module.js';// Default importimport myDefault from './module.js';// Default + named importsimport myDefault, { a, b } from './module.js';// Import all as namespaceimport * as MyModule from './module.js';// Side-effect importimport './module.js';
// utils/format.jsexport function formatDate(date) { /* ... */ }export function formatCurrency(amount) { /* ... */ }// utils/validate.jsexport function isEmail(str) { /* ... */ }export function isPhone(str) { /* ... */ }// utils/index.js — re-exports everythingexport { formatDate, formatCurrency } from './format.js';export { isEmail, isPhone } from './validate.js';// Now in main.js, you can import from the folderimport { formatDate, isEmail } from './utils/index.js';// Or even shorter (works with bundlers and Node.js, not native browser modules):import { formatDate, isEmail } from './utils';
// counter.jslet counter = 0; // Private (not exported)export function increment() { counter++;}export function getCount() { return counter;}// main.jsimport { increment, getCount } from './counter.js';increment();console.log(getCount()); // 1// counter variable is not accessible at all
// ✗ Bad: One file does everything// utils.js with 50 different functions// ✓ Good: Separate concerns// formatters.js — formatting functions// validators.js — validation functions// api.js — API calls
// user/// ├── User.js # User class// ├── userService.js # User API calls// ├── userUtils.js # User-related utilities// └── index.js # Re-exports public API
4. Consider Default Exports for Components/Classes
A common convention is to use default exports when a module has one main purpose:
Copy
Ask AI
// Components are usually one-per-file// Button.jsexport default function Button({ label, onClick }) { return <button onClick={onClick}>{label}</button>;}// Usage is cleanimport Button from './Button.js';
// Multiple utilities in one file// stringUtils.jsexport function capitalize(str) { /* ... */ }export function truncate(str, length) { /* ... */ }export function slugify(str) { /* ... */ }// Import only what you needimport { capitalize } from './stringUtils.js';
One of the most common sources of confusion is mixing up how to import named vs default exports:
Copy
Ask AI
┌─────────────────────────────────────────────────────────────────────────┐│ NAMED vs DEFAULT EXPORT CONFUSION │├─────────────────────────────────────────────────────────────────────────┤│ ││ EXPORTING IMPORTING ││ ───────── ───────── ││ ││ Named Export: Must use { braces }: ││ export function greet() {} import { greet } from './mod.js' ││ export const PI = 3.14 import { PI } from './mod.js' ││ ││ Default Export: NO braces: ││ export default function() {} import greet from './mod.js' ││ export default class User {} import User from './mod.js' ││ ││ ⚠️ Common Error: ││ import greet from './mod.js' ← Looking for default, but file has ││ named export! Results in undefined ││ │└─────────────────────────────────────────────────────────────────────────┘
Copy
Ask AI
// utils.js — has a NAMED exportexport function formatDate(date) { return date.toLocaleDateString()}// ❌ WRONG — Importing without braces looks for a default exportimport formatDate from './utils.js'console.log(formatDate) // undefined! No default export exists// ✓ CORRECT — Use braces for named exportsimport { formatDate } from './utils.js'console.log(formatDate) // [Function: formatDate]
The Trap: If you see undefined when importing, check whether you’re using braces correctly. Named exports require { }, default exports don’t. This is the #1 cause of “why is my import undefined?” bugs.
Circular dependencies occur when two modules import from each other. This creates a “chicken and egg” problem that causes subtle, hard-to-debug issues:
Copy
Ask AI
┌─────────────────────────────────────────────────────────────────────────┐│ CIRCULAR DEPENDENCY │├─────────────────────────────────────────────────────────────────────────┤│ ││ user.js userUtils.js ││ ┌──────────┐ ┌──────────────┐ ││ │ │ ──── imports from ────► │ │ ││ │ User │ │ formatUser() │ ││ │ class │ ◄─── imports from ───── │ createUser() │ ││ │ │ │ │ ││ └──────────┘ └──────────────┘ ││ ││ 🔄 PROBLEM: When user.js loads, it needs userUtils.js ││ But userUtils.js needs User from user.js ││ Which isn't fully loaded yet! → undefined ││ │└─────────────────────────────────────────────────────────────────────────┘
Copy
Ask AI
// ❌ PROBLEM: Circular dependency// user.jsimport { formatUserName } from './userUtils.js'export class User { constructor(name) { this.name = name }}// userUtils.jsimport { User } from './user.js' // Circular! user.js imports userUtils.jsexport function formatUserName(user) { return user.name.toUpperCase()}export function createDefaultUser() { return new User('Guest') // 💥 User might be undefined here!}
Copy
Ask AI
// ✓ SOLUTION: Break the cycle with restructuring// user.js — no imports from userUtilsexport class User { constructor(name) { this.name = name }}// userUtils.js — imports from user.js (one direction only)import { User } from './user.js'export function formatUserName(user) { return user.name.toUpperCase()}export function createDefaultUser() { return new User('Guest') // Works! User is fully loaded}
Rule of Thumb: Draw your import arrows. They should flow in one direction like a tree, not in circles. If module A imports from B, module B should NOT import from A. If you need shared code, create a third module that both can import from.
Try to answer each question before revealing the solution:
Question 1: What does IIFE stand for and why was it invented?
Answer: IIFE stands for Immediately Invoked Function Expression.It was invented to solve the problem of global scope pollution. Before ES6 modules, all JavaScript code shared the same global scope. Variables from different files could accidentally overwrite each other. IIFEs create a private scope where variables are protected from outside access.
Question 2: What's the difference between named exports and default exports?
Answer:Named exports:
Can have multiple per module
Must be imported by exact name (or aliased)
Use export { name } or export function name()
Import with import { name } from './module.js'
Default exports:
Only one per module
Can be imported with any name
Use export default
Import with import anyName from './module.js'
Copy
Ask AI
// Named exportexport const PI = 3.14;import { PI } from './math.js';// Default exportexport default function add(a, b) { return a + b; }import myAdd from './math.js'; // Any name works
Question 3: How do you create a private variable in an IIFE?
Answer: Declare the variable inside the IIFE. It won’t be accessible from outside because it’s in the function’s local scope.
Copy
Ask AI
const module = (function() { // Private variable let privateCounter = 0; // Return public methods that can access it return { increment() { privateCounter++; }, getCount() { return privateCounter; } };})();module.increment();console.log(module.getCount()); // 1console.log(module.privateCounter); // undefined (private!)
Question 4: What's the difference between static and dynamic imports?
// Static import — always at the top, always loadedimport { heavyFunction } from './heavy-module.js'// Dynamic import — loaded only when neededasync function loadOnDemand() { const module = await import('./heavy-module.js') module.heavyFunction()}// Or with .then() syntaximport('./heavy-module.js').then(module => { module.heavyFunction()})
Use dynamic imports for code splitting and loading modules on demand.
Question 5: Why should you avoid circular dependencies?
Answer: Circular dependencies occur when module A imports from module B, and module B imports from module A.Problems:
Loading issues: When A loads, it needs B. But B needs A, which isn’t fully loaded yet.
Undefined values: You might get undefined for imports that should have values.
Confusing bugs: Hard to track down because the error isn’t where the bug is.
Solution: Create a third module for shared code, or restructure your code to break the cycle.
Question 6: When would you still use an IIFE today?
Answer: Even with ES6 modules, IIFEs are useful for:
Async initialization:
Copy
Ask AI
(async () => { const data = await fetchData(); init(data);})();
One-time calculations:
Copy
Ask AI
const config = (() => { // Complex setup that runs once return computedConfig;})();
Scripts without modules: When you’re adding a <script> tag without type="module", IIFEs prevent polluting globals.
An IIFE (Immediately Invoked Function Expression) is a function that runs as soon as it is defined. It creates a private scope that prevents variables from leaking into the global namespace. As documented on MDN, the pattern wraps a function in parentheses to make it an expression, then immediately invokes it with ().
Why were IIFEs used before ES6 modules?
Before ES6 introduced native modules in 2015, JavaScript had no built-in way to create private scope at the file level. IIFEs provided encapsulation by leveraging function scope and closures. Libraries like jQuery and Lodash used the IIFE pattern extensively to avoid polluting the global namespace.
What is the difference between IIFEs and ES6 modules?
ES6 modules provide file-level scope automatically — every file is its own module with private variables. IIFEs achieve the same result manually using function scope. According to the 2023 State of JS survey, ES modules are now used by over 80% of JavaScript developers, making IIFEs largely unnecessary for new code. However, IIFEs remain useful for one-time initialization and inline scripts.
What is a namespace in JavaScript?
A namespace is an object that groups related variables and functions under a single global name to avoid naming conflicts. Before modules, developers used patterns like var MyApp = MyApp || {} to organize code. The namespace pattern reduced global pollution but did not provide true privacy, which is why the module pattern and later ES6 modules became preferred.
Are IIFEs still useful in modern JavaScript?
Yes, in specific cases. IIFEs are still valuable for async initialization ((async () => { ... })()), one-time configuration, and scripts loaded without type="module". They also appear frequently in build tool output and legacy codebases, so understanding them remains important for professional JavaScript development.