SECTION 1
Historical Context & Motivation
In the early days of web development, JavaScript was written entirely inside <script> tags placed directly in HTML files. Every variable and function you created lived in a single global scope, which meant that as projects grew larger, names would collide, bugs became harder to track, and code was nearly impossible to reuse across projects. Imagine trying to organize a school project where every team member dumps their notes into one giant, unsorted pile — that was the state of early JavaScript.
Developers needed a way to split code into separate, self-contained files — each responsible for a specific task — and then connect those files together cleanly. This idea is called modularity, and it is one of the most important principles in software engineering. Over time, several community-driven solutions emerged before the language itself adopted an official standard.
2009 • CommonJS Introduced
require() and module.exports, allowing server-side JavaScript to load files on demand.2011 • AMD & RequireJS
2015 • ES2015 (ES6) Specification
export and import keywords — no libraries required.2017–2018 • Browser Support Arrives
<script type="module">, making ES Modules usable directly in browsers without a bundler.2020+ • Universal Standard
The central question ES Modules answer is: how can we write JavaScript in separate files, share only the parts we choose, and keep everything else private? Understanding the answer unlocks the ability to build larger, cleaner, more maintainable programs.
SECTION 2
Core Principles & Definitions
Before diving into code, let's establish the foundational ideas behind ES Modules. Each principle below builds on the previous one, so read them in order.
Module = File
.js file is treated as its own module. Variables and functions inside a file are private by default — nothing leaks out unless you explicitly share it.export — Share Outward
export keyword marks a function, variable, or class as available to other files. Think of it as putting an item on a shelf that others can take.import — Bring In
import keyword lets another file grab specific exports. You choose exactly what you need — no more, no less.Named vs. Default
Static Structure
KEY TAKEAWAY
export keyword is like putting specific items on display in a window, and import is like reaching through that window to grab exactly what you need. You can never accidentally take something that wasn't displayed — that's what makes modules safe and predictable.SECTION 3
Visual Explanation — How Modules Connect
The diagram below shows a typical project with three files. The math.js module exports two named functions. The greeting.js module exports one default function. The app.js file imports from both modules, using only what it needs.
import to pull in only the pieces it needs.Notice that app.js uses curly braces { add, multiply } for named imports but no curly braces for the default import greet. This syntax difference is one of the most common sources of confusion for beginners, so keep an eye on it as we explore further.
SECTION 4
How ES Modules Work Under the Hood
When a browser or Node.js encounters an ES Module, it goes through three distinct phases before your code actually runs. Understanding these phases helps explain why certain rules exist — like why you cannot put import statements inside if blocks.
Phase 1 — Parsing & Construction
The engine reads through all of your files and builds a module graph — a map that shows which files depend on which other files. It finds every import statement and fetches the corresponding file. Because imports must appear at the top level (not inside functions or conditionals), the engine can build this map before running a single line of your code.
Phase 2 — Instantiation
The engine allocates memory for every exported binding. Think of this step as labeling empty shelves: the engine knows the names of everything that will be exported, but the values haven't been assigned yet. Importantly, imports and exports are live bindings — the importing file doesn't get a copy of the value; it gets a live reference to the original variable in the exporting module.
Phase 3 — Evaluation
Finally, the engine executes each module's code in depth-first order, starting from modules with no dependencies (leaf nodes) and working back to the entry point. Each module is evaluated only once, even if multiple files import it. The values fill the shelves created in Phase 2.
SECTION 5
Named Exports vs. Default Exports — A Detailed Breakdown
ES Modules give you two flavors of exporting: named exports and default exports. You can even mix both in the same file. The table below compares them side by side so you can see when to use each approach.
| Feature | Named Export | Default Export |
|---|---|---|
| Syntax (exporting) | export function add() { } or export { add, multiply }; | export default function greet() { } |
| Syntax (importing) | import { add } from './math.js'; | import greet from './greeting.js'; |
| How many per file? | Unlimited — as many as you need | At most one per file |
| Curly braces? | Required: { name } | Not used — any name works |
| Renaming on import | import { add as sum } from '...' | You choose the name freely: import sayHello from '...' |
| Best for | Utility libraries with many exports (e.g., math helpers) | Files with one primary purpose (e.g., a component) |
Named vs. Default exports comparison
Inline Export vs. Export List
Named exports can be written in two equivalent ways. You can place the export keyword directly in front of each declaration (inline style), or you can declare everything normally and then add a single export list at the bottom of the file. Both produce the same result, so choose whichever style your team prefers for readability.
Common Mistake Alert
import { Add } from './math.js' will fail if the module exports add (lowercase). JavaScript is case-sensitive! Use the as keyword if you need a different name: import { add as Add } from './math.js'.SECTION 6
Worked Example — Building a Mini Utility Library
Let's walk through creating a small project that uses both named and default exports. We'll build a temperature converter library and an entry-point file that uses it.
Temperature Converter with ES Modules
- 1
Step 1 — Create the converter module (converter.js)
We create a file calledconverter.jsand define two named exports: one function to convert Fahrenheit to Celsius and another to convert Celsius to Fahrenheit.// converter.js export function toCelsius(f) { return (f - 32) * 5 / 9; } export function toFahrenheit(c) { return c * 9 / 5 + 32; }Two named exports:toCelsiusandtoFahrenheit - 2
Step 2 — Create a greeting module (greeting.js)
We create a second module with a single purpose — generating a formatted greeting message. Since the file has one main job, we use a default export.// greeting.js export default function greet(city) { return `Welcome to the weather report for ${city}!`; }One default export:greet - 3
Step 3 — Import everything in the entry point (app.js)
Inapp.js, we import the named exports using curly braces and the default export without braces. Notice the file paths start with./to indicate a local file.// app.js import { toCelsius, toFahrenheit } from './converter.js'; import greet from './greeting.js';Named imports use{ }; default imports do not. - 4
Step 4 — Use the imported functions
We call the imported functions just like any other function. Everything is scoped — no global pollution.console.log(greet('Denver')); // → "Welcome to the weather report for Denver!" console.log(toCelsius(98.6)); // → 37 console.log(toFahrenheit(0)); // → 32Output: "Welcome to the weather report for Denver!", 37, 32 - 5
Step 5 — Run as a module in the browser
To use ES Modules in an HTML file, add thetype="module"attribute to your script tag. Without this attribute, the browser treats the file as a regular script and will throw errors onimportstatements.<script type="module" src="./app.js"></script>Thetype="module"attribute is required for ES Modules to work in browsers.
SECTION 7
Strengths & Limitations of ES Modules
ES Modules are powerful, but like any tool they come with trade-offs. The table below summarizes the key advantages and limitations you should be aware of when using them in real projects.
| Strengths ✅ | Limitations ⚠️ |
|---|---|
| Official language standard — works everywhere without extra libraries | Older browsers (IE 11 and below) do not support ES Modules |
| Static analysis enables tree-shaking, reducing bundle size | Cannot conditionally import (no import inside if blocks — must use dynamic import() for that) |
| Encapsulation — variables stay private unless exported | Local file paths must include the file extension (.js) in browsers |
| Live bindings keep values in sync across files | Modules always run in strict mode — no with statements, no silent errors |
| Clear dependency graph makes debugging easier | Circular dependencies (A imports B which imports A) can cause confusion |
Strengths and limitations of ES Modules
KEY TAKEAWAY
SECTION 8
Connection to Advanced Module Patterns
Now that you understand static import and export, it's worth knowing that JavaScript also supports more advanced module patterns. These build on the same ES Module system but add flexibility for complex situations.
| Concept | What You Learned (Intro) | Advanced Version |
|---|---|---|
| Static Import | import { add } from './math.js' | Dynamic Import: const mod = await import('./math.js') — loads modules on demand at runtime |
| Individual Imports | import { add, multiply } from ... | Namespace Import: import * as math from './math.js' — bundles all exports into one object |
| Direct Export | Each file exports its own code | Re-export: export { add } from './math.js' — a file can forward exports from another module |
| Single Entry | One app.js file imports everything | Barrel Files: An index.js re-exports from many modules, creating a clean public API |
Introductory vs. advanced module concepts
You don't need to master these advanced patterns right now, but knowing they exist gives you a roadmap. As your projects grow — especially if you use frameworks like React or Vue — you'll encounter dynamic imports for code-splitting, barrel files for clean library APIs, and namespace imports for large utility modules. The foundation you've built today makes learning those patterns much easier.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
Show answer
Question 2: BASIC CALCULATION
import statement to use square in another file:
// mathUtils.js
export function square(n) {
return n * n;
}
export function cube(n) {
return n * n * n;
}Show answer
square is a named export, you must use curly braces: import { square } from './mathUtils.js';. If you also wanted cube, you would write import { square, cube } from './mathUtils.js';. You could also rename on import using import { square as sq } from './mathUtils.js'; if you preferred a shorter name.Question 3: INTERMEDIATE
// logger.js
export default function log(msg) {
console.log(`[LOG]: ${msg}`);
}
// app.js
import { log } from './logger.js';
log('Hello!');Show answer
log function is exported as a default export, but the student is importing it with curly braces as if it were a named export. Default imports must not use curly braces. The fix is: import log from './logger.js';. This is one of the most common mistakes beginners make — always check whether the module uses export default or just export.Question 4: APPLIED
validators.js (provides isEmail and isNotEmpty functions), formHandler.js (a default export of a function that processes form data), and main.js (the entry point). Write all three files with proper export/import statements. The formHandler should use the validators internally.Show answer
// validators.js
export function isEmail(str) {
return str.includes('@');
}
export function isNotEmpty(str) {
return str.trim().length > 0;
}
// formHandler.js
import { isEmail, isNotEmpty } from './validators.js';
export default function handleForm(data) {
if (!isNotEmpty(data.name)) return 'Name is required';
if (!isEmail(data.email)) return 'Invalid email';
return 'Form submitted!';
}
// main.js
import handleForm from './formHandler.js';
const result = handleForm({ name: 'Alex', email: 'alex@school.edu' });
console.log(result); // → 'Form submitted!'
Notice how formHandler.js acts as both an importer (bringing in validators) and an exporter (providing handleForm to main.js). This chaining is a natural pattern in real projects.Question 5: CRITICAL THINKING
utils.js, but app.js only needs 3 of them. A teammate suggests using import * as utils from './utils.js' for convenience. Evaluate this approach: what are the advantages and disadvantages compared to importing only the 3 functions by name? How does this relate to tree-shaking?Show answer
import * as utils (a namespace import) is convenient because you don't need to list each function, and all 50 are accessible via utils.functionName. However, it has significant disadvantages. First, tree-shaking — the process by which bundlers like Webpack or Vite remove unused code — works best with individual named imports. When you write import { a, b, c } from './utils.js', the bundler can confidently remove the other 47 functions from the final build. With a namespace import, some bundlers can still tree-shake, but the analysis is more complex and less reliable. Second, named imports make it immediately clear which functions a file depends on, improving readability and maintainability. The best practice is to import only what you need by name: it keeps your code explicit and your bundles small.SUMMARY
Lesson Summary
Summary
ES Modules are JavaScript's official system for splitting code into reusable files. Each file is a module where variables and functions are private by default. You use the export keyword to make specific values available to other files and the import keyword to bring those values in. There are two flavors: named exports (imported with curly braces, unlimited per file) and default exports (imported without braces, one per file).
The module system works through three loading phases: parsing (building the dependency graph), instantiation (creating live bindings), and evaluation (running the code). This static structure enables powerful optimizations like tree-shaking. To use modules in the browser, add type="module" to your script tag. Mastering ES Modules is a foundational skill for writing organized, maintainable JavaScript in any modern project.