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.

7 min read

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)
Created2009 for Node.js2015 as ECMAScript standard
Export syntaxmodule.exports or exportsexport or export default
Import syntaxconst x = require("...")import { x } from "..."
LoadingSynchronous at runtimeAsynchronous, parsed at compile time
File extension.js or .cjs.mjs or .js with type: module
This at top levelthis === module.exportsthis === undefined
Tree shakingNot possibleYes, with bundlers
Browser supportNo (needs bundler)Yes (native)
CommonJS vs ES modules loading flow

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:

javascriptjavascript
// 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:

javascriptjavascript
// 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:

javascriptjavascript
// 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:

javascriptjavascript
// 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:

javascriptjavascript
// 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:

javascriptjavascript
// 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"; // Error

When 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:

javascriptjavascript
// 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

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.
RunePowered by Rune AI

Frequently Asked Questions

Which module system should I use for a new Node.js project?

Use ES modules. They are the modern standard, supported by all tools, and work identically in browsers and Node.js. Set type: module in package.json or use .mjs extension. Only use CommonJS if you need a library that has not yet published an ES module version.

Can I import a CommonJS module from an ES module?

Yes. Node.js lets you import CommonJS modules using the default import syntax. The entire module.exports object becomes the default export. However, named imports from CommonJS modules are not supported because CJS does not have static named exports.

Why does Node.js still support CommonJS?

Millions of existing packages use CommonJS, and the Node.js ecosystem was built on it for over a decade. Removing support would break nearly every existing application. The transition to ES modules is gradual and both systems coexist.

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.