Skip to content
  • Modules: ESM vs CommonJS · JavaScript

Modules: ESM vs CommonJS

LearnViaNotes @learnvianotes· 2 min read

Modules give each file its own scope and explicit imports/exports. JavaScript has two systems: standard ES modules and Node's older CommonJS.

ES module syntax

JavaScript
// math.js
export const PI = 3.14159;
export function area(r) { return PI * r * r; }
export default class Calculator { /* ... */ }

// app.js
import Calculator, { PI, area as circleArea } from "./math.js";
import * as math from "./math.js";
export { circleArea };                    // re-export
export * from "./other.js";
const { heavy } = await import("./heavy.js");   // dynamic import (returns a promise)

Exports are live bindings, which a runnable demo can show by importing an inline module from a data: URL:

JavaScript
const source = `
  export let count = 0;
  export function increment() { count++; }
  export default "the default export";
`;
const mod = await import("data:text/javascript," + encodeURIComponent(source));
console.log(mod.default, mod.count);
mod.increment();
console.log(mod.count);                   // live binding: sees the update
try {
  mod.count = 99;                         // importers cannot assign
} catch (e) {
  console.log(e.name);
}
console.log(Object.keys(mod));
Output
the default export 0
1
TypeError
[ 'count', 'default', 'increment' ]

ESM vs CommonJS

ES modules (ESM)CommonJS (CJS)
Syntaximport / exportrequire() / module.exports
LoadingStatic graph, parsed before running; async-capableSynchronous, at runtime
Top-level awaitYesNo
BindingsLive, read-onlyA copied value (exports object)
Strict modeAlwaysOnly with "use strict"
Top-level thisundefinedmodule.exports
File detection in Node.mjs, or .js with "type": "module".cjs, or .js by default
__dirname, requireNot defined — use import.meta.dirname, createRequireAvailable
Tree-shakingYes (static analysis)Hard
JavaScript
import { createRequire } from "node:module";
import path from "node:path";

const require = createRequire(import.meta.url);     // CJS require inside ESM
const os = require("node:os");
console.log(typeof os.platform, typeof require.resolve);
console.log(path.basename(import.meta.filename), import.meta.url.startsWith("file://"));
console.log(typeof __dirname, typeof module, this);
Output
function function
main.mjs true
undefined undefined undefined

CommonJS for reference

JavaScript
// counter.cjs
let count = 0;
module.exports = { count, increment() { count++; } };

// app.cjs
const counter = require("./counter.cjs");
counter.increment();
console.log(counter.count);   // 0 — a copy was exported, not a live binding

Module facts worth knowing

  • A module is evaluated once, then cached — every importer gets the same instance (natural singletons).

  • Imports are hoisted and resolved before the module body runs; circular imports see TDZ bindings until the other module finishes.

  • In browsers: <script type="module"> is deferred by default, runs in strict mode and needs full URLs or an import map.

  • Dynamic import() enables code splitting and lazy loading; it works in CJS too.

  • require(esm) is supported in modern Node (22+) for ES modules without top-level await.

What are the main differences between CommonJS and ES modules?

Free for students and teachers

Write notes like this one

Cards for every idea, quizzes and flashcards, read aloud — on the web and your phone.

Something wrong with this note? Open it to report