Skip to main content
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?
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!

What is an IIFE?

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.
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.

The Messy Desk Problem: A Real-World Analogy

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:
This is the story of how JavaScript developers learned to organize their code:
  1. First, we had the messy desk — everything in the global scope
  2. Then, we invented IIFEs — a clever trick to create private spaces
  3. Next, we created Namespaces — grouping related things under one name
  4. Finally, we got Modules — the modern, built-in solution
Let’s learn each approach and understand when to use them.

Part 1: IIFE — The Self-Running Function

Breaking Down the Name

The acronym IIFE tells you exactly what it does:
  • Immediately — runs right now
  • Invoked — called/executed
  • Function Expression — a function written as an expression (not a declaration)

Expression vs Statement: Why It Matters for IIFEs

To understand IIFEs, you need to understand the difference between expressions and statements in JavaScript.
The key insight: A function can be written two ways:
Arrow functions are always expressions:
Why does this matter for IIFEs?
Function Declaration vs Function Expression:

The Anatomy of an IIFE

Let’s break down the syntax piece by piece:
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.”

IIFE Variations

There are several ways to write an IIFE. They all do the same thing:

Why Were IIFEs Invented?

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:
IIFEs solved this by creating a private scope:

Practical Example: Creating Private Variables

One of the most powerful uses of IIFEs is creating private variables that can’t be accessed from outside:
This pattern is called the Module Pattern. It uses closures to keep variables private. It was the standard way to create “modules” before ES6.

IIFE with Parameters

You can pass values into an IIFE:

When to Use IIFEs Today

With ES6 modules, IIFEs are less common. But they’re still useful for:

Part 2: Namespaces — Organizing Under One Name

What is a Namespace?

A namespace is a container that groups related code under a single name. It’s like putting all your kitchen items in a drawer labeled “Kitchen.”

Why Use Namespaces?

Creating a Namespace

The simplest namespace is just an object:

Nested Namespaces

For larger applications, you can nest namespaces:

Combining Namespaces with IIFEs

The best of both worlds: organized AND private:
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.

Part 3: ES6 Modules — The Modern Solution

What are Modules?

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.

Why Modules are Better

How to Use Modules

In the Browser

In Node.js

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.

Exporting: Sharing Your Code

There are two types of exports: named exports and default exports.

Named Exports

Named exports let you export multiple things from a module. Each has a name.

Default Export

Each module can have ONE default export. It’s the “main” thing the module provides.

When to Use Each

Use when:
  • You’re exporting multiple things
  • You want clear, explicit imports
  • You want to enable tree-shaking

Importing: Using Other People’s Code

Named Imports

Import specific things by name (must match the export names):

Default Import

Import the default export with any name you choose:
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.

Side-Effect Imports

Sometimes you just want to run a module’s code without importing anything:

Import Syntax Summary


Organizing a Real Project

Let’s see how modules work in a realistic project structure:

The Index.js Pattern (Barrel Files)

Use index.js to re-export from multiple files:

Real Example: A Simple App


Dynamic Imports

Sometimes you don’t want to load a module until it’s needed. Dynamic imports load modules on demand:
Performance tip: Dynamic imports are great for loading heavy libraries only when needed. This makes your app’s initial load faster.

The Evolution: From IIFEs to Modules

Here’s how the same code would look in each era:

Common Patterns and Best Practices

1. One Thing Per Module

Each module should do one thing well:

3. Avoid Circular Dependencies

4. Consider Default Exports for Components/Classes

A common convention is to use default exports when a module has one main purpose:

5. Use Named Exports for Utilities


Common Mistakes to Avoid

Mistake 1: Confusing Named and Default Exports

One of the most common sources of confusion is mixing up how to import named vs default exports:
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.

Mistake 2: Circular Dependencies

Circular dependencies occur when two modules import from each other. This creates a “chicken and egg” problem that causes subtle, hard-to-debug issues:
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.

Key Takeaways

The key things to remember:
  1. IIFEs create private scope by running immediately — useful for initialization and avoiding globals
  2. Namespaces group related code under one object — reduces global pollution but isn’t true encapsulation
  3. ES6 Modules are the modern solution — file-based, true privacy, and built into the language
  4. Named exports let you export multiple things — import what you need by name
  5. Default exports are for the main thing a module provides — one per file
  6. Dynamic imports load modules on demand — great for performance optimization
  7. Each module has its own scope — variables are private unless exported
  8. Use modules for new projects — IIFEs and namespaces are for legacy code or special cases
  9. Organize by feature or type — group related modules in folders with index.js barrel files
  10. Avoid circular dependencies — they cause confusing bugs and loading issues

Test Your Knowledge

Try to answer each question before revealing the solution:
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.
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'
Answer: Declare the variable inside the IIFE. It won’t be accessible from outside because it’s in the function’s local scope.
Answer:Static imports:
  • Loaded at the top of the file
  • Always loaded, even if not used
  • Analyzed at build time
  • Syntax: import { x } from './module.js'
Dynamic imports:
  • Can be loaded anywhere in the code
  • Loaded only when the import() call runs
  • Loaded at runtime, returns a Promise
  • Syntax: const module = await import('./module.js')
Use dynamic imports for code splitting and loading modules on demand.
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.
Answer: Even with ES6 modules, IIFEs are useful for:
  1. Async initialization:
  1. One-time calculations:
  1. Scripts without modules: When you’re adding a <script> tag without type="module", IIFEs prevent polluting globals.
  2. Creating private scope in non-module code.

Frequently Asked Questions

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 ().
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.
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.
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.
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.

Scope and Closures

Understanding how JavaScript manages variable access and function memory

Higher-Order Functions

Functions that work with other functions — common in modular code

Design Patterns

Common patterns for organizing code, including the module pattern

Call Stack

How JavaScript tracks function execution and manages memory

Reference

IIFE — MDN

Official MDN documentation on Immediately Invoked Function Expressions

JavaScript Modules — MDN

Complete guide to ES6 modules

Expression Statement — MDN

MDN documentation on expression statements

Namespace — MDN

MDN documentation on namespaces

Articles

Mastering Immediately-Invoked Function Expressions

Covers the classical and Crockford IIFE variations with clear syntax breakdowns. Great for understanding why the parentheses are placed where they are.

JavaScript Modules: A Beginner's Guide

Traces the evolution from global scripts to CommonJS to ES6 modules with code examples at each stage. Perfect if you’re wondering why we have so many module formats.

A 10 minute primer to JavaScript modules

Explains the difference between module formats (AMD, CommonJS, ES6), loaders (RequireJS, SystemJS), and bundlers (Webpack, Rollup). Clears up the confusing terminology quickly.

ES6 Modules in Depth

Nicolás Bevacqua’s thorough exploration of edge cases like circular dependencies and live bindings. Read this after you understand the basics.

JavaScript modules — V8

The V8 team’s comprehensive guide covering native module loading, performance recommendations, and future developments. Includes practical tips on bundling vs unbundled deployment.

Modules — javascript.info

Interactive tutorial walking through module basics with live code examples. Covers both browser and Node.js usage patterns with clear, beginner-friendly explanations.

All you need to know about Expressions, Statements and Expression Statements

Explains why function(){}() fails but (function(){})() works. The expression vs statement distinction finally makes sense after reading this.

Function Expressions — MDN

MDN’s official reference on function expressions, covering syntax, hoisting behavior differences from declarations, and named function expressions. Includes interactive examples.

Videos

Immediately Invoked Function Expression — Beau teaches JavaScript

Short and focused 4-minute explanation perfect for quick learning. Part of freeCodeCamp’s beginner-friendly JavaScript series.

JavaScript Modules: ES6 Import and Export

Kyle from Web Dev Simplified builds a project step-by-step showing named exports, default exports, and barrel files. Great for seeing modules in action.

JavaScript IIFE — Steve Griffith

Demonstrates the Module Pattern with private variables and public methods. Shows exactly how closures make IIFEs powerful.

ES6 Modules in the Real World

Conference talk on practical module usage in production applications.

Expressions vs. Statements in JavaScript

Uses simple examples to show why expressions produce values and statements perform actions. Essential for understanding IIFE syntax.

JavaScript Functions — Programming with Mosh

Comprehensive overview of JavaScript functions covering declarations, expressions, hoisting, and scope. Clear explanations with practical examples.
Last modified on February 17, 2026