Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Modules in JavaScript  ›  Capstone Project

ESM Modules

ESM 12 minES modules use static import/export statements that must sit at the top level and can't be conditional, and their exports are live bindings that stay connected to the source, unlike CommonJS's copied snapshots.

ESM Modules

In short: ESM (ECMAScript Modules) is the standardised import/ export syntax browsers and modern Node both understand natively. import and export are static declarations — the engine reads and resolves them before any code runs, so they must sit at a file's top level and can never be conditional, inside an if or a function. In return, ESM exports are live bindings: an imported name stays genuinely connected to its source, and updates automatically when the source changes it — the opposite of CommonJS's one-time copy. This lesson's code needs real files on disk to run — read it, don't run it.

Two boxes at the top: a valid import { count, increment } from "./counter.mjs" (must sit at the top, never inside an if) next to if (true) { import {...} } throwing a SyntaxError because imports are static, not a runtime call; below, counter.mjs's export let count = 0 wired with a "live wire" to main.mjs, where count reads 0, then 1 after increment() — ESM's binding stays connected, unlike CommonJS which copies the value out once

Watch an imported value update itself

Import a variable, call a function that changes it, and the imported name reflects the change — no re-import needed:

// counter.mjs
export let count = 0;
export function increment() { count++; }

// main.mjs
import { count, increment } from "./counter.mjs";
console.log(count); // 0
increment();
console.log(count); // 1 — updated automatically

This is the defining difference from CommonJS: count here isn't a copied value, it's a live binding to the actual variable inside counter.mjs. Every file that imports count sees the same, current value, always.

The trap: trying to import conditionally

Because import is resolved statically — before the module's code even starts running — it cannot depend on a runtime condition:

if (Math.random() > 0.5) {
  import { count } from "./counter.mjs"; // SyntaxError
}

This fails to even parse: import is only legal as a top-level declaration, never nested inside an if, a function, a loop, or any other block. Needing a module conditionally at runtime calls for dynamic import() instead — a function-like form that returns a promise: const mod = await import("./counter.mjs");.

Everyday differences worth knowing

A few more contrasts that matter in practice: ESM runs in strict mode automatically, with no "use strict" needed. Browsers load ESM files with <script type="module" src="main.mjs">; Node recognises them by a .mjs extension or "type": "module" in package.json. ESM also supports top-level await directly in a module's own body, something CommonJS's synchronous require has no equivalent for.

Quick practice

quick drill

What does import { count } from "./counter.mjs"; increment(); console.log(count); print, given export let count = 0; and an increment() that does count++?

quick drill

Why does if (true) { import { count } from "./counter.mjs"; } fail?

quick drill

What's the ESM equivalent of Node's conditional, runtime-only require()?

quick drill

Which statement is true about ESM's strict mode?

quick drill

What is the key behavioural difference between an ESM export and a CommonJS export?

🎯 Your turn

Given counter.mjs with export let count = 0; export function increment() { count++; }, and main.mjs with import { count, increment } from "./counter.mjs"; increment(); console.log(count);, explain what gets printed and why this differs from the equivalent CommonJS example, and explain why if (Math.random() > 0.5) { import { count } from "./counter.mjs"; } is not allowed.

Write your explanation and submit it — there's no code to run here, just your reasoning.

Practice — level up

0/1 solved

Write your answer, then compare it with a rubric and a model answer.

ExplainLive binding, not a snapshot

Given counter.mjs with export let count = 0; export function increment() { count++; }, and main.mjs with import { count, increment } from "./counter.mjs"; increment(); console.log(count);, explain what gets printed and why this differs from the equivalent CommonJS example, and explain why if (Math.random() > 0.5) { import { count } from "./counter.mjs"; } is not allowed.