CommonJS vs ES Modules in JavaScript Guide
Compare CommonJS and ES modules side by side. Learn the syntax differences, loading behavior, interoperability rules, and when to use each module system in Node.js and the browser.
JavaScript has two major module systems: CommonJS and ES modules. They solve the same fundamental problem of splitting code across files, but they differ in syntax, loading behavior, and where they work. Understanding both is essential because real projects often mix them.
CommonJS is the older system, created for Node.js before JavaScript had native modules. ES modules are the official standard, now supported in every modern browser and Node.js. The table below captures the core differences at a glance.
| CommonJS (CJS) | ES Modules (ESM) | |
|---|---|---|
| Created | 2009 for Node.js | 2015 as ECMAScript standard |
| Export syntax | module.exports or exports | export or export default |
| Import syntax | const x = require("...") | import { x } from "..." |
| Loading | Synchronous at runtime | Asynchronous, parsed at compile time |
| File extension | .js or .cjs | .mjs or .js with type: module |
| This at top level | this === module.exports | this === undefined |
| Tree shaking | Not possible | Yes, with bundlers |
| Browser support | No (needs bundler) | Yes (native) |
CommonJS resolves and executes modules synchronously as the code runs. ES modules build the entire dependency graph first, then execute code. This architectural difference explains most of the behavioral distinctions between the two systems.
Syntax Comparison
The syntax difference is the most visible change. CommonJS uses a function call pattern. ES modules use declarative statements.
Exporting a value in CommonJS assigns to the module.exports object:
// math.cjs
function add(a, b) {
return a + b;
}
module.exports = { add };Exporting the same value in ES modules uses the export keyword placed directly before the function declaration statement itself, like this:
// math.mjs
export function add(a, b) {
return a + b;
}Importing follows the same pattern. CommonJS calls require as a function at any point in the code. ES modules use static import declarations at the top of the file:
// CommonJS
const { add } = require("./math.cjs");
// ES modules
import { add } from "./math.mjs";Because CommonJS require is a function call, you can use it conditionally or dynamically anywhere in your code. ES module imports must be static top-level declarations, though dynamic import() provides runtime loading when needed.
Default Exports
CommonJS does not have a native concept of default exports. The convention is to assign directly to module.exports for a single export:
// greet.cjs
module.exports = function greet(name) {
return `Hello, ${name}!`;
};
// app.cjs
const greet = require("./greet.cjs");ES modules have explicit default export syntax. Importing a CommonJS module from an ES module maps module.exports to the default import:
// Importing CJS from ESM - module.exports becomes the default
import greet from "./greet.cjs";Named imports from CommonJS modules do not work from ES modules because CommonJS exports are a dynamic object, not a set of static bindings. For this reason, libraries that want to support both module systems often provide dual packages.
Synchronous vs Asynchronous Loading
CommonJS loads modules synchronously. When Node.js encounters require, it pauses execution, resolves the file, loads and executes it, and returns the exports. This works well on a server where file access is fast, but it would block the browser's main thread.
ES modules load asynchronously. The browser or runtime fetches all imported modules in parallel, builds a dependency graph, and only executes code after every module is ready. This enables native browser support without bundlers.
The synchronous nature of CommonJS is why you can use require inside an if statement or function. The static nature of ES modules is why import must be at the top level and tree shaking works.
Interoperability in Node.js
Node.js supports importing between the two systems with specific rules. Understanding these rules prevents frustrating import errors.
When an ES module imports a CommonJS module, the entire module.exports object becomes the default export. Named imports are not supported because the bundler cannot statically analyze a dynamic object:
// This works - CJS module.exports becomes default
import pkg from "./legacy.cjs";
// This does NOT work - named imports need static analysis
import { add } from "./legacy.cjs"; // ErrorWhen a CommonJS module imports an ES module, you must use dynamic import because require is synchronous and ES modules are asynchronous. CommonJS files do not support top-level await, so wrap the import in an async function:
// In a CJS file, import an ESM module asynchronously
async function loadModernModule() {
const { add } = await import("./modern.mjs");
return add;
}This async requirement means migrating a large codebase from CommonJS to ES modules is often done incrementally. New files use ES module syntax, and older files are imported dynamically where needed.
When to Use Which
For new projects in 2026, default to ES modules. They are the standard, work everywhere, and enable modern tooling features like tree shaking and static analysis. Set "type": "module" in your package.json and use import and export everywhere.
Use CommonJS when you are maintaining an existing Node.js project that has not been migrated, or when a critical dependency has not published an ES module version. In these cases, keep new files in CommonJS until the dependency landscape supports a full migration.
For library authors, publish dual packages that support both systems. The exports field in package.json maps import and require to different entry points. This lets consumers use your library regardless of their module system.
For more on the ES module syntax, see the ES6 modules guide. For tree shaking and bundle optimization, see tree shaking and advanced code splitting.
Rune AI
Key Insights
- CommonJS uses require and module.exports; ES modules use import and export.
- CommonJS loads synchronously at runtime; ES modules load asynchronously and are parsed statically.
- ES modules enable tree shaking and static analysis; CommonJS does not.
- Node.js supports both systems with interoperability rules for cross-imports.
- For new projects, default to ES modules with type: module in package.json.
Frequently Asked Questions
Which module system should I use for a new Node.js project?
Can I import a CommonJS module from an ES module?
Why does Node.js still support CommonJS?
Conclusion
CommonJS and ES modules solve the same problem with different approaches. CommonJS is synchronous, dynamic, and deeply embedded in the Node.js ecosystem. ES modules are static, asynchronous, and the future of JavaScript. For new projects, use ES modules. For existing projects, migrate gradually. Understanding both systems makes you effective in any JavaScript codebase.
More in this topic
Using Decorators for Logging in JS Architecture
Learn how JavaScript decorators wrap class methods to add logging without touching the original code, including setup with Babel and how execution order works.
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.