ES modulesESMimport/exportJavaScript modulesnative modules
TL;DR
The official JavaScript module system — import and export statements enable static dependency graphs, tree-shaking, and native browser module loading without a bundler.
Explanation
ES Modules (ESM), standardised in ES2015, replaced the CommonJS require/module.exports pattern. Named exports (export const x = ...) and default exports (export default ...) are imported with import { x } from './mod.js' and import Foo from './mod.js' respectively. ESM is statically analysed — imports are resolved at parse time, not runtime, enabling bundlers to tree-shake unused exports. In browsers, <script type="module"> loads ESM natively with automatic defer semantics and strict mode. In Node.js, ESM files use .mjs extension or "type": "module" in package.json. Key differences from CommonJS: ESM imports are live bindings (reflect mutations in the exporting module), CJS exports are copied values. Dynamic import() returns a Promise and enables code-splitting — loading a module on demand rather than at startup. Import maps (supported in modern browsers) allow remapping bare specifiers like 'react' to a CDN URL without a bundler.
Watch Out
⚠ Node.js does not allow require() inside ESM modules — if a dependency is CJS-only, you must use dynamic import() or stay in CJS. The error 'require is not defined in ES module scope' is a common migration surprise.
Common Misconception
✗ ESM imports are live bindings — if the exporting module later mutates an exported variable, the importing module sees the updated value. This is the opposite of CommonJS, which copies the value at require() time.
Why It Matters
ESM is the foundation of modern JavaScript tooling — tree-shaking, code splitting, and native browser loading all depend on ESM's static structure. Mixing CJS and ESM in Node.js projects is a frequent source of confusing errors.
Common Mistakes
Omitting the file extension in Node.js ESM imports — 'import x from './utils'' fails; the extension '.js' is required (no automatic resolution).
Using require() in a .mjs file or a package with type:module — CJS and ESM cannot be mixed without dynamic import().
Re-exporting a default as a named export — import Foo from './a'; export { Foo } is needed; export { default } from './a' is the cleaner form.
Expecting top-level this to be the module object — in ESM, top-level this is undefined (strict mode), not the global or module object.
Code Examples
💡 Note
The CJS version loads the entire date-fns library at runtime; the ESM version lets bundlers include only the format function. In the browser, <script type="module"> defers automatically and scopes the module — no global pollution.
✗ Vulnerable
// CommonJS — no tree-shaking, runtime resolution:
const { format } = require('date-fns');
module.exports = { formatDate };
✓ Fixed
// ESM — statically analysable, tree-shakeable:
import { format } from 'date-fns'; // bundler can drop unused date-fns exports
export function formatDate(date) {
return format(date, 'yyyy-MM-dd');
}
// In Node.js: file must be .mjs or package.json must have "type": "module"