Warming up the neural circuits...
By the end of this chapter, you will stop writing 'scripts' and start architecting highly scalable 'modules'. You will:
In the early days of JavaScript, every script tag added variables directly to a single, global namespace (the window object). If two scripts used the same variable name, they would collide, overwriting each other's data and crashing the application.
ES Modules (ESM) are the official ECMA standard for organizing code into independent, reusable files. Every module has its own Private Scope. Variables, classes, and functions only escape their file boundary if you explicitly export them. This allows developers to build massive, maintainable codebases and enables tools like Vite, Webpack, and Rollup to perform advanced optimizations like Tree Shaking—automatically deleting unused code from your production bundles.
Modern frameworks rely heavily on ESM dynamic imports to achieve automatic code splitting. By asynchronously importing a component or screen only when a user navigates to it, the browser doesn't need to load the entire website's code at startup—it only loads exactly what is required for the active screen.
Use this decision tree to determine how to organize and request your module contents:
Rendering diagram…
Named exports mandate exact matching names during imports. This strict contract makes named exports highly secure, easy for IDE autocompletions to resolve, and extremely simple for bundlers to tree-shake out of bundles if they are not imported anywhere.
// utils.js
export const PI = 3.14159;
export function multiply(a, b) { return a * b; }
// main.js - import requires curly braces
import| Feature | ES Modules (ESM) | CommonJS (CJS) | Dynamic Import (import()) |
|---|---|---|---|
| Syntax | import / export | require / module.exports | import('file.js') |
| Parsing Phase | Static (Compile-time) | Dynamic (Runtime) | Asynchronous (Runtime) |
| Returns | Binding reference | Plain exported Object | Promise (resolves to module) |
| File Scope | Private Scope per file | Wrapper Function Scope | Private Scope per file |
| Supported in Node? | ✅ Yes (requires config) | ✅ Yes (Native Default) | ✅ Yes |
Integrating modules correctly is critical for implementing lazy routing and optimizing asset size.
| Mistake | Technical Reason | Visual Console Error | The Fix |
|---|---|---|---|
| Missing file extension (Node) | Node.js ES Module path resolution requires explicit extensions, unlike CommonJS. | TypeError [ERR_MODULE_NOT_FOUND]: Cannot find module 'C:\app\utils' | Append .js directly to local imports (import { x } from './utils.js'). |
| Circular Dependencies | File A imports B, and B imports A; this causes circular evaluation lockups. | ReferenceError: Cannot access 'X' before initialization | Extract the shared variables/methods to a third file (e.g. shared.js). |
| CJS globals in ESM | Global helpers like __dirname or __filename are not defined in ES Modules scope. | ReferenceError: __dirname is not defined | Use import.meta.url with the standard url and path packages to resolve paths. |
database.js (which exports a named class ConnectionPool) and queries.js (which exports a named function fetchUsers). Write an index.js barrel file that re-exports both modules from a single entry facade../database.js -> ConnectionPool
./queries.js -> fetchUsersexport { ConnectionPool } from './database.js';
export { fetchUsers } from './queries.js';__dirname variable using import.meta.url to resolve the absolute path to a file named db.sqlite located in the same folder.// Your code hereAbsolute C:\...\db.sqlite path resolved as a string.triggerAnalytics(eventName) that lazily imports a module from ./tracker.js only when invoked. The dynamic module exports a named function track(event). If the import fails, catch it and log a friendly warning.triggerAnalytics("Purchase");Tracker loaded on-demand and executed, errors safely caught.dbInstance.import { dbInstance } from './databasePool.js';dbInstance is guaranteed to be fully resolved and connected when imported by parent files.What does import() (dynamic import) return?
A default export does not require curly braces during imports. It represents the "main product" of the file. However, default exports allow the importer to rename the import to anything, which can make global codebase searches more difficult.
// button.js
export default function PrimaryButton() { return 'Button Component'; }
// main.js - No curly braces, can be renamed freely
import MyCustomButton from './button.js';as syntax)If there is a naming collision between two imports, you can rename them:
import { render as renderReact } from 'react-dom';
import { render as renderAngular } from '@angular/core';index.js)As projects grow, importing components from multiple sub-directories creates messy import headers. A Barrel is an file that acts as a single front gate, re-exporting modules from neighboring files.
// components/Button.js
export function Button() {}
// components/Card.js
export function Card() {}
// components/index.js (The Barrel File)
export { Button } from './Button.js';
export { Card } from './Card.js';
// app.js - One simple line instead of two!
import { Button, Card } from './components/index.js';import *)You can group all named exports from a file into a single local namespace object.
import * as MathOps from './math.js';
console.log(MathOps.PI);
console.log(MathOps.add(5, 5));import())Unlike static imports (which must sit at the absolute top of the file and load synchronously), a dynamic import can be called anywhere inside your code. It returns a that resolves to the module object.
const loadFeatureBtn = document.getElementById('load-btn');
loadFeatureBtn.addEventListener('click', async () => {
try {
// Asynchronously downloads the file only when the user clicks!
const { initializeChart } = await import('./chart.js');
initializeChart();
} catch (error) {
console.error("Failed to load modular chart bundle", error);
}
});You can use the await keyword directly inside a module without wrapping it in an async function. This is highly useful for loading database configuration files or dynamic translation properties before the of the application bootstrap logic loads.
// config.js
// Blocks parent module execution until the dynamic configuration is fetched!
const userSettings = await fetch('/api/settings').then(res => res.json());
export { userSettings };In React, dynamic imports are combined with lazy to split monolithic component bundles into separate chunks.
import React, { lazy, Suspense } from 'react';
// ✅ Heavy component is dynamically requested only when rendered!
const CodeEditor = lazy(() => import('./CodeEditor.js'));
export function IDEContainer() {
return (
<div>
<h3>Advanced Code Workspace</h3>
<Suspense fallback={<div>Loading Editor Assets...</div>}>
<CodeEditor />
</Suspense>
</div>
);
}require is a CommonJS dynamic runtime API; it does not exist in static ESM files. |
ReferenceError: require is not defined |
Convert CJS require statements into standard ESM import statements. |
| Infinite Top-level await | Fetching a slow resource at the top-level blocks the entire module resolution graph. | None, but app experiences long white screen boot delays | Use regular async/await inside runtime hooks or functions, or add execution timeouts. |
store.js to decouple the dependencies cleanly.// A.js
import { name } from './B.js';
export const uppercaseName = name.toUpperCase();
// B.js
import { uppercaseName } from './A.js';
export const name = "Alice";No ReferenceErrors are thrown, and variables compile cleanly.