CommonJS vs ES Modules
Node now ships two module systems side by side, and choosing — or mixing — them is a real source of friction. This page lays them out feature by feature, explains the deep reason they behave differently, and gives concrete guidance for new projects and migrations.
Side by side
Aspect | CommonJS | ES Modules |
|---|---|---|
Import syntax |
|
|
Export syntax |
|
|
Loading | Synchronous, on demand | Asynchronous, statically resolved |
When imports resolve | At runtime, where written | Hoisted, before execution |
Conditional/dynamic import | Yes — | Only via |
Bindings | Copies of values | Live, read-only bindings |
Top-level await | No | Yes |
| Provided | Use |
File extension in imports | Optional | Required |
Tree-shaking | Hard (dynamic) | Easy (static) |
The root cause: static vs dynamic
Almost every row above flows from one design choice. CommonJS resolves modules at runtime — require is just a function that runs when reached, so it can be conditional and synchronous but cannot be analyzed ahead of time. ESM resolves the entire graph statically, before executing any code, which enables tree-shaking and live bindings but forbids runtime-computed imports.
// CommonJS — decided while running const mod = require(condition ? './a' : './b') // ESM — the static form cannot do that; use the dynamic function form const mod = await import(condition ? './a.js' : './b.js')
How Node decides which system a file is
File | package.json | Result |
|---|---|---|
| anything | CommonJS |
| anything | ES Modules |
|
| ES Modules |
|
| CommonJS |
Mixing the two
ESM → CJS: works.
import pkg from "cjs-lib"gives youmodule.exportsas the default. Named imports work when Node can statically detect them, but fall back toconst { x } = pkgif not.CJS → ESM:
require()of an ESM file throwsERR_REQUIRE_ESM(ESM is async,requireis sync). Use dynamicimport()instead — it returns a promise.Dual packages: libraries ship both builds via the
"exports"field with"import"and"require"conditions, so each consumer gets the right one.
CommonJS reaching an ESM module
// require('./esm-only.mjs') → ERR_REQUIRE_ESM
const esm = await import('./esm-only.mjs') // this works (async)Which should you use?
Migration cheat-sheet
CommonJS | ES Module equivalent |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|