A
AiTechWorlds
AiTechWorlds
ES Modules (ESM) are JavaScript's built-in module system. They let you split code into files, share functionality between them, and avoid the global scope pollution that plagued early JavaScript. Every modern JavaScript project uses them.
// math.js — export multiple things
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
export function multiply(a, b) { return a * b; }
export const PI = 3.14159;
// Or export at the end (often preferred for readability)
function divide(a, b) {
if (b === 0) throw new Error("Division by zero");
return a / b;
}
const E = 2.71828;
export { divide, E };
// main.js — import specific exports
import { add, multiply, PI } from "./math.js";
console.log(add(2, 3)); // 5
console.log(multiply(4, PI)); // 12.56636
// user.js — one default export per file
export default class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
toString() {
return `${this.name} <${this.email}>`;
}
}
// Or a function
// api.js
export default async function fetchUser(id) {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
// Import default — any name you want
import User from "./user.js";
import fetchUser from "./api.js";
import getUser from "./api.js"; // same import, different name — works
const alice = new User("Alice", "alice@example.com");
// config.js
export const timeout = 5000;
export const retries = 3;
const config = { timeout, retries, version: "2.0" };
export default config;
// Import both
import config, { timeout, retries } from "./config.js";
// Rename on export
export { add as sum, multiply as product };
// Rename on import (avoids naming conflicts)
import { add as mathAdd } from "./math.js";
import { add as arrayAdd } from "./array-utils.js";
* as)import * as math from "./math.js";
math.add(1, 2); // 5
math.PI; // 3.14159
math.default; // the default export (if any)
A common pattern: index.js collects and re-exports from multiple files, creating a clean public API:
// utils/string.js
export function capitalize(str) { return str[0].toUpperCase() + str.slice(1); }
export function truncate(str, len) { return str.length > len ? str.slice(0, len) + "..." : str; }
// utils/array.js
export function unique(arr) { return [...new Set(arr)]; }
export function chunk(arr, size) { /* ... */ }
// utils/index.js — re-export everything
export { capitalize, truncate } from "./string.js";
export { unique, chunk } from "./array.js";
export { default as deepClone } from "./deep-clone.js";
// Now consumers import from one place
import { capitalize, unique, deepClone } from "./utils/index.js";
Load modules on demand — great for code splitting and lazy loading:
// Static imports are evaluated at load time
import { heavyLibrary } from "./heavy.js"; // loaded immediately
// Dynamic import — returns a Promise
async function loadChart() {
const { Chart } = await import("./chart.js");
return new Chart();
}
// Conditional loading
if (userIsAdmin) {
const { AdminPanel } = await import("./admin.js");
AdminPanel.mount();
}
// In Next.js / React for code splitting
const HeavyComponent = React.lazy(() => import("./HeavyComponent"));
There are two key differences when using type="module":
<!-- Script mode — global scope, synchronous -->
<script src="old.js"></script>
<!-- Module mode — module scope, deferred by default -->
<script type="module" src="main.js"></script>
In module mode:
this at the top level is undefined (not window)defer attribute)// package.json — use "type": "module" for ESM
{
"type": "module"
}
// Node.js ESM — must include file extension
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
// __dirname doesn't exist in ESM — replace with:
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Feature detection with dynamic import
const module = await import(
navigator.onLine ? "./online-module.js" : "./offline-module.js"
);
// Import JSON (Node.js 22+)
import data from "./config.json" with { type: "json" };
// Circular imports — JavaScript handles them but they can be tricky
// If A imports B and B imports A, be careful about initialization order
Next lesson: Optional Chaining & Nullish Coalescing — safe property access without crashing.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises