Complete interview answer for ESM vs CommonJS covering static vs dynamic loading, live bindings, Node package exports, interop pitfalls, and why module format affects bundling, optimization, and runtime behavior.
ESM vs CommonJS in JavaScript: Interop, Packaging, and Build Behavior
Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.
Definition (above the fold)
ES Modules (ESM) are the standard JavaScript module system with static import/export syntax that tools can analyze ahead of time. CommonJS (CJS) is the older Node.js module system using require and module.exports. Interviewers care less about syntax and more about interop and packaging decisions in real projects.
Mental model
Think of ESM as statically analyzable graph edges and CJS as runtime-evaluated module loading. Static analysis enables better tree-shaking and optimization. Runtime flexibility in CJS can help legacy systems but complicates analysis.
Dimension | ESM | CommonJS |
|---|---|---|
Import style |
|
|
Analysis | Static graph known at build time | More dynamic, harder to optimize |
Bindings | Live bindings | Exported object values |
Async loading | Native support with | Primarily synchronous |
Tooling fit | Best for modern bundlers and browsers | Strong legacy Node ecosystem support |
Runnable example #1: equivalent module exports
// math.mjs (ESM)
export const add = (a, b) => a + b;
export default function sum(arr) {
return arr.reduce((acc, n) => acc + n, 0);
}
// math.cjs (CommonJS)
const addCjs = (a, b) => a + b;
function sumCjs(arr) {
return arr.reduce((acc, n) => acc + n, 0);
}
module.exports = { add: addCjs, sum: sumCjs };
Same behavior is possible in both systems, but the package boundary and import style define interoperability and optimization quality.
Runnable example #2: package exports map for safer consumption
{
"name": "ui-kit",
"type": "module",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
},
"main": "./dist/index.cjs",
"module": "./dist/index.js"
}
Interop pitfall | Symptom | Fix |
|---|---|---|
Default vs named import mismatch |
| Align export style and import syntax explicitly |
Mixed module entrypoints | Consumers resolve wrong file in bundlers | Use consistent |
Transpiler masking module type issues | Works in dev but breaks in production runtime | Validate with runtime-native tests |
Dynamic require in shared package | Poor tree-shaking and larger bundles | Prefer static imports where possible |
Common pitfalls
- Publishing ambiguous package entrypoints without an
exportsmap. - Assuming all tools resolve CJS/ESM the same way.
- Using CJS patterns in ESM codebases and losing optimization opportunities.
- Not testing package consumption in both import and require scenarios.
When to use / when not to use
Use ESM by default for modern frontend and new libraries. Keep CJS compatibility only when you must support legacy runtimes or tooling. Do not keep dual formats indefinitely without ownership, because dual-package maintenance can create drift and subtle bugs.
Interview follow-ups
Q1: Why does ESM help optimization? A: Static imports allow bundlers to prune unused code more safely.
Q2: How do you ship both formats? A: Build dual outputs and define deterministic exports conditions.
Q3: What breaks most often? A: Default/named interop mismatches and inconsistent runtime/tool resolver behavior.
Implementation checklist / takeaway
Prefer ESM-first packaging, define explicit exports conditions, test import/require consumers, and avoid ambiguous entrypoints. Strong interview answers connect module format to runtime correctness and bundle quality.