JavaScript Modules: ESM vs CommonJS
14, July 2026 - By Admin

ES Modules and CommonJS are the two dominant module systems in the JavaScript ecosystem. Though they solve the same problem -- splitting code into reusable, encapsulated files -- they differ fundamentally in syntax, resolution timing, and loading behavior.
Modern JavaScript applications are built from many small files that import and export functionality from one another. Two module systems dominate this landscape: **CommonJS (CJS)**, the format Node.js has used since its early days, and **ECMAScript Modules (ESM)**, the standardized module system built into the JavaScript language itself since ES2015.
While both accomplish the same goal of organizing code into reusable units, they differ in syntax, in when module dependencies are resolved, and in how they're loaded at runtime. Understanding these differences matters when configuring a Node.js project, debugging interop issues, or deciding how a library should ship its code.
ES Modules are the language's standard; CommonJS is the ecosystem's long-standing default -- knowing both, and how they interoperate, is essential for working in modern Node.js and bundler tooling.- JS Expert
Static vs. Dynamic Resolution
**ES Modules** use `import` and `export` statements that must appear at the top level of a file, with the module specifier written as a static string literal. This lets JavaScript engines and bundlers analyze the entire dependency graph **before** any code runs -- a process called **static analysis**. It's what enables tree-shaking (removing unused exports) and catching missing imports at build time rather than at runtime.
**CommonJS**, by contrast, resolves `require()` calls **dynamically**, at the moment the line of code executes. Because `require()` is just a regular function call, it can appear anywhere -- inside conditionals, loops, or functions -- and the module path can be computed at runtime. This flexibility comes at the cost of static analyzability.
Synchronous vs. Asynchronous Loading
CommonJS's `require()` is **synchronous**: when a module is required, Node.js reads, compiles, and executes the file immediately, blocking until it's done, then returns `module.exports`. This works well for local filesystem access, which is what Node.js was originally designed around.
ES Modules are designed to support **asynchronous** loading, which allows them to work in environments like the browser where modules may need to be fetched over the network. The `import()` function (as opposed to the static `import` statement) returns a Promise, enabling dynamic, on-demand loading of modules without blocking execution.
Choosing Between ESM and CommonJS
For new projects, **ES Modules are generally the recommended choice**. They're the official standard, work natively in modern browsers without a build step, support static analysis and tree-shaking for smaller bundles, and are fully supported by current Node.js LTS versions when a package sets `"type": "module"` in its `package.json`.
**CommonJS remains relevant** for maintaining older Node.js codebases, working with packages that haven't migrated to ESM, and writing simple scripts or tooling where synchronous, dynamic `require()` calls are convenient. Framework tooling such as Next.js also still relies on CommonJS internally for parts of its build pipeline, even when application code is authored using ESM syntax.
A Note on File Extensions and package.json
Node.js determines how to interpret a `.js` file based on the nearest `package.json`'s `"type"` field: `"type": "module"` treats `.js` files as ESM, while its absence (or `"type": "commonjs"`) treats them as CommonJS. The unambiguous extensions `.mjs` and `.cjs` override this setting file-by-file, which is useful when a single project needs to mix both formats intentionally.
Key Differences at a Glance:
- **Syntax:** CommonJS uses `require()` and `module.exports`; ES Modules use `import` and `export`.
- **Resolution:** ESM resolves imports statically at parse time; CommonJS resolves `require()` calls dynamically at runtime.
- **Loading:** CommonJS loads modules synchronously; ESM supports both static, synchronous-style imports and asynchronous dynamic `import()`.
- **Value binding:** ESM exports are live, read-only bindings to the original values; CommonJS exports a copy of whatever value is on `module.exports` at the time it was required.
- **File treatment:** Node.js treats `.mjs` files (or `.js` files in a package with `"type": "module"`) as ESM, and `.cjs` files (or plain `.js` without that field) as CommonJS.
How Node.js and Bundlers Handle Interop:
1 - Node.js lets an ES Module import a CommonJS module directly -- the CommonJS `module.exports` object becomes the default export:
2 - The reverse is not directly possible -- a CommonJS file cannot `require()` a true ES Module synchronously. It must use the asynchronous `import()` function instead:
3 - Bundlers like webpack and frameworks like Next.js smooth over most of this by transpiling ESM syntax to a CommonJS-compatible module format (or vice versa) at build time, and by adding interop helpers (like `__esModule` markers) so `import` and `require` can mix safely within an application's source.
Ahmed Hamada
A JavaScript enthusiast who loves exploring different patterns and techniques to improve code efficiency.
