ESM vs CommonJS, In Practice

The real interop rules: named-export detection, top-level await, and the require(esm) thaw.

Node runs both module systems, and the friction is at the boundary. Know the actual rules, not the folklore.

Importing CommonJS from ESM

You can import a CJS module. Node analyzes it and exposes module.exports as the default export, and does a best-effort static scan to also offer named exports. That scan can miss things assigned dynamically — when a named import fails on a CJS package, fall back to import pkg from '…'; const { thing } = pkg;.

Importing ESM from CommonJS

Historically CJS could only reach ESM via dynamic await import(), because require is synchronous and ESM can have top-level await. Recent Node (22+, and stabilized in 24-era) added require(esm) for ESM graphs with no top-level await — a big ergonomics win, but await import() remains the always-safe path.

// CJS reaching into an ESM-only package, the portable way:
async function main() {
  const { default: chalk } = await import('chalk');
  console.log(chalk.green('works from CommonJS'));
}
main();

The differences that bite

  • No __dirname/__filename in ESM — use import.meta.dirname / import.meta.filename (or derive from import.meta.url).
  • ESM has top-level await; CJS does not.
  • ESM import specifiers need file extensions and are resolved statically; require is dynamic and extension-optional.

Example

Example · javascript
// A dual-published package's exports map, plus the ESM idioms that replace
// CommonJS globals. This is package.json — shown as an example for context.
//
// {
//   "name": "@acme/toolkit",
//   "type": "module",
//   "exports": {
//     ".": {
//       "types": "./dist/index.d.ts",
//       "import": "./dist/index.js",     // ESM consumers
//       "require": "./dist/index.cjs"    // CommonJS consumers
//     },
//     "./package.json": "./package.json"
//   }
// }

// index.js (ESM) — replacing __dirname and reading a sibling file:
import { readFile } from 'node:fs/promises';

// import.meta.dirname (Node 20.11+/21.2+) — no url/path dance needed.
const here = import.meta.dirname;
const pkg = JSON.parse(
  await readFile(new URL('./config.json', import.meta.url), 'utf8'),
);

// Top-level await is legal in ESM — great for one-time async setup.
const secrets = await loadSecrets();

export function greet(name) { return `hi ${name} from ${here}`; }
async function loadSecrets() { return { ok: true }; }

When to use it

  • A developer importing a pure-ESM package into a CJS project uses dynamic `import()` as the only way to load it, since `require()` cannot load ESM.
  • A library author configures the `exports` field in `package.json` to serve `.mjs` for ESM consumers and `.cjs` for CJS consumers from the same source.
  • A team migrating a CJS codebase to ESM incrementally uses `.mjs` extensions for new files while keeping `.js` files as CJS during the transition.

More examples

Dynamic import to load ESM from CJS

Uses dynamic `import()` -- the only way for a CJS module to consume a pure-ESM package.

Example · js
// CJS file (no "type":"module" in package.json)
async function main() {
  // ESM-only packages can't be require()'d
  const { default: chalk } = await import('chalk');
  console.log(chalk.green('Hello from ESM in CJS!'));
}
main();

Dual package exports map

Configures the `exports` map to serve different entry files for `import` (ESM) vs `require` (CJS) consumers.

Example · json
{
  "name": "my-lib",
  "exports": {
    ".": {
      "import":  "./dist/index.mjs",
      "require": "./dist/index.cjs",
      "types":   "./dist/index.d.ts"
    }
  }
}

Detect module system at runtime

Shows how to detect whether code is running as ESM or CJS by checking for `import.meta.url` vs `require`/`__dirname`.

Example · js
// In a .js file with "type":"module" (ESM):
console.log(typeof require);   // 'undefined'
console.log(typeof import.meta.url); // 'string'

// In a .js file without type:module (CJS):
console.log(typeof require);   // 'function'
console.log(typeof __dirname); // 'string'

Discussion

  • Be the first to comment on this lesson.