Home JavaScript CommonJS vs ES Modules in Node.js — A Complete Guide
Intermediate 5 min · 2026-07-12

CommonJS vs ES Modules in Node.js — A Complete Guide

CommonJS vs ES Modules in Node.js explained: require vs import, differences in resolution, named vs default exports, interoperability patterns, and migration strategies..

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.

Follow
Production
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 15-20 minutes
  • Basic Node.js and JavaScript knowledge. Familiarity with npm and Express.js is recommended.
 ● Production Incident
Quick Answer

Node.js supports two module systems: CommonJS (require/module.exports) and ES Modules (import/export). CommonJS is synchronous, loads modules at runtime, and has been the default since Node's inceptio

✦ Definition~90s read
What is CommonJS vs ES Modules in Node.js?

Node.js supports two module systems: CommonJS (require/module.exports) and ES Modules (import/export). CommonJS is synchronous, loads modules at runtime, and has been the default since Node's inception. ES Modules are the official JavaScript standard, are asynchronous, support static analysis (tree-shaking), and require 'type': 'module' in package.json or .mjs extensions.

Imagine you're moving into a new house.

The two systems have different resolution algorithms, different handling of named vs default exports, and different top-level this values. Interoperability is possible via default imports of CJS into ESM and createRequire for using CJS from ESM.

Plain-English First

Imagine you're moving into a new house. CommonJS is like having all your boxes pre-labeled and stacked in the garage — you grab what you need, but everything is already there when you start. ES Modules are like having a smart home system: you tell the system 'I need the couch' and it fetches it from the warehouse only when you sit down. CommonJS loads everything upfront (synchronous), while ES Modules load on demand (asynchronous), making your app start faster and use less memory.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

The import statement looks cleaner. Everyone says ES Modules are the future. So you add 'type': 'module' to package.json, your Express app crashes with require is not defined, and you spend two hours debugging why __dirname is undefined. The reality is that Node.js runs two parallel module systems, and understanding the difference — not just the syntax — is essential for shipping production code in 2026. CommonJS is not going away, and ES Modules have sharp edges that even experienced engineers hit. This article covers the practical differences, not just the syntax.

Module Systems in Node.js: The Two Standards

Node.js supports two module systems: CommonJS (CJS) and ES Modules (ESM). CommonJS, the original system, uses require() and module.exports. ES Modules, the modern standard, uses import and export. Both coexist, but they differ fundamentally in syntax, behavior, and ecosystem support. Understanding these differences is critical for writing maintainable, performant Node.js applications. CommonJS is synchronous and loads modules at runtime, while ESM is asynchronous and supports static analysis, enabling tree-shaking and better optimization. The Node.js team recommends ESM for new projects, but CommonJS remains widespread in legacy code and npm packages.

cjs-example.jsJAVASCRIPT
1
2
3
// CommonJS
const fs = require('fs');
module.exports = { readFile: fs.readFileSync };
Try it live
🔥ESM is the future
Node.js 12+ supports ESM natively. New projects should default to ESM for better tooling and performance.
📊 Production Insight
Mixing CJS and ESM can cause subtle bugs. In a production microservice, we once had a circular dependency that only manifested in ESM due to its different resolution order.
🎯 Key Takeaway
CommonJS is synchronous and runtime-based; ESM is asynchronous and static-analysis-friendly.
commonjs-vs-es-modules THECODEFORGE.IO Module System Architecture in Node.js Layered view of CJS and ESM interoperability Application Code ESM imports | CJS requires Module Resolution File extension rules | Package type detection Loader Layer ESM loader | CJS loader | createRequire bridge Runtime Execution Static analysis (ESM) | Dynamic require (CJS) Module Cache Module._cache (CJS) | Module map (ESM) THECODEFORGE.IO
thecodeforge.io
Commonjs Vs Es Modules

Syntax and Export Differences

CommonJS uses module.exports to export a single value (often an object) and require() to import. ES Modules use named exports (export const) and default exports (export default), imported via import. ESM exports are live bindings — changes to the exported value in the exporting module are reflected in the importing module. CommonJS exports are copies. This distinction matters for mutable objects. Also, ESM supports top-level await, which CommonJS does not. The syntax differences affect how you structure your code and handle dependencies.

esm-example.mjsJAVASCRIPT
1
2
3
// ES Module
export const greet = (name) => `Hello, ${name}!`;
export default function() { return 'default'; }
Try it live
💡Named exports for clarity
Prefer named exports over default exports in ESM for better refactoring and IDE support.
📊 Production Insight
We once had a bug where a config object mutated in one module didn't reflect in another because it was a CJS copy. Switching to ESM fixed it.
🎯 Key Takeaway
ESM exports are live bindings; CJS exports are copies. This affects mutable state sharing.

File Extensions and Package Type

Node.js determines the module system based on file extension and nearest package.json. Files with .mjs are always ESM; .cjs are always CommonJS; .js files default to CommonJS unless the nearest package.json has "type": "module". This setting applies to all .js files in the package. You can override per file with .mjs/.cjs. This flexibility allows gradual migration but can cause confusion when dependencies have mixed types.

package.jsonJSON
1
2
3
4
5
6
{
  "type": "module",
  "dependencies": {
    "lodash": "^4.17.21"
  }
}
⚠ Extension confusion
Forgetting to set "type": "module" in package.json while using ESM syntax will cause a SyntaxError.
📊 Production Insight
In a monorepo, we had packages with different type settings. A shared utility was imported as CJS in one and ESM in another, causing duplicate instances and memory bloat.
🎯 Key Takeaway
Use .mjs for ESM, .cjs for CJS, or set "type": "module" in package.json for .js files.
commonjs-vs-es-modules THECODEFORGE.IO Module Resolution Architecture in Node.js Layered system for CJS and ESM interoperability Application Code ESM imports | CJS requires Module Loader ESM loader | CJS loader Resolution Algorithm File extension detection | Package type checks Interop Layer createRequire | named exports shim Runtime V8 module system | CommonJS wrapper THECODEFORGE.IO
thecodeforge.io
Commonjs Vs Es Modules

Interoperability: Importing CJS from ESM and Vice Versa

ESM can import CommonJS modules using the default import syntax: import pkg from 'cjs-pkg'. Named imports from CJS are also supported via a static analysis of the CJS module's exports. However, CJS cannot use import to load ESM — it must use dynamic import(), which returns a promise. This asymmetry is a common pain point. Also, ESM imports are static and must be at the top level, while import() is dynamic. When migrating a large CJS codebase to ESM, you'll often need to use dynamic imports for ESM modules inside CJS files.

cjs-import-esm.cjsJAVASCRIPT
1
2
3
4
5
6
// CommonJS importing ESM
async function loadESM() {
  const esmModule = await import('./esm-module.mjs');
  console.log(esmModule.default());
}
loadESM();
Try it live
🔥Dynamic import is your friend
Use import() in CJS to load ESM modules. It's asynchronous, so handle promises carefully.
📊 Production Insight
In a serverless function, we used import() inside a CJS handler to load an ESM library. The async overhead caused cold starts to increase by 200ms.
🎯 Key Takeaway
ESM can import CJS statically; CJS can only import ESM dynamically via import().

Circular Dependencies: How Each System Handles Them

Circular dependencies occur when module A imports module B, and B imports A (directly or indirectly). CommonJS handles this by returning a partial copy of the module's exports at the time of the require. This can lead to undefined values if the circular dependency is not carefully managed. ES Modules handle circular dependencies better because exports are live bindings — the importing module gets a reference to the exported value, which may still be undefined if the export hasn't been initialized yet. However, both systems can fail if not designed carefully. The best practice is to avoid circular dependencies altogether.

circular-cjs.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
// a.js
const b = require('./b');
console.log(b); // {}
module.exports = { name: 'A' };

// b.js
const a = require('./a');
console.log(a); // { name: 'A' }? No, it's {} because a hasn't finished exporting yet.
module.exports = { name: 'B' };
Output
{}
Try it live
⚠ Circular dependencies are code smell
Refactor to eliminate cycles. Use dependency injection or extract shared logic into a third module.
📊 Production Insight
A payment processing system had a circular dependency between Order and Invoice modules. In CJS, it caused intermittent undefined errors. Refactoring into a shared service eliminated the issue.
🎯 Key Takeaway
ESM handles circular deps better with live bindings, but both are fragile. Avoid cycles.

Performance and Tree-Shaking

ES Modules enable static analysis, which allows bundlers like Webpack and Rollup to perform tree-shaking — removing unused exports to reduce bundle size. CommonJS modules are dynamic, so tree-shaking is harder. In Node.js runtime, ESM has slightly higher startup overhead due to asynchronous loading, but this is negligible for most applications. For server-side code, the performance difference is minimal. However, for libraries published to npm, providing both CJS and ESM builds (dual packages) is common to support all consumers.

tree-shaking-example.jsJAVASCRIPT
1
2
3
4
5
6
7
// utils.js
export const used = () => 'used';
export const unused = () => 'unused'; // will be removed by tree-shaking

// main.js
import { used } from './utils.js';
console.log(used());
Output
used
Try it live
💡Dual packages for libraries
Publish your library with both main (CJS) and module (ESM) fields in package.json to maximize compatibility.
📊 Production Insight
Switching a React app from CJS to ESM reduced the production bundle by 30% due to tree-shaking unused lodash functions.
🎯 Key Takeaway
ESM enables tree-shaking, reducing bundle size. CJS is harder to optimize statically.

Migrating from CJS to ESM: A Practical Strategy

Migrating a large codebase from CommonJS to ES Modules requires careful planning. Start by setting "type": "module" in package.json and renaming files to .mjs incrementally. Use dynamic import() for CJS files that need to load ESM modules. Update all require() calls to import statements, and change module.exports to export default or named exports. Be aware of differences like __dirname and __filename — in ESM, use import.meta.url and fileURLToPath. Also, note that require.resolve is not available in ESM; use createRequire from module if needed. Test thoroughly, especially for circular dependencies and interop with third-party packages.

esm-dirname.mjsJAVASCRIPT
1
2
3
4
5
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
console.log(__dirname);
Output
/path/to/current/dir
Try it live
🔥Gradual migration
You can have both .cjs and .mjs files in the same project. Migrate one module at a time.
📊 Production Insight
During migration of a 500-file backend, we missed a require inside a try-catch that broke error handling. Use lint rules to catch remaining CJS syntax.
🎯 Key Takeaway
Migrate incrementally: set type, rename files, update imports/exports, and handle __dirname replacement.

Common Pitfalls and Best Practices

Common pitfalls include: forgetting to add "type": "module", using require in ESM (throws ReferenceError), assuming __dirname exists, and circular dependencies. Best practices: default to ESM for new projects, use named exports, avoid default exports for better tree-shaking, and always specify file extensions in imports (e.g., ./module.js not ./module). For dual-package libraries, use the exports field in package.json to map CJS and ESM entry points. Also, be careful with JSON imports — in ESM, you need to use assert { type: 'json' } (Node 17+) or createRequire.

esm-json-import.mjsJAVASCRIPT
1
2
import data from './config.json' assert { type: 'json' };
console.log(data);
Output
{ "key": "value" }
Try it live
⚠ JSON imports require assertion
In Node <17, use createRequire to load JSON in ESM. In Node 17+, use import assertions.
📊 Production Insight
A missing file extension in an import caused a module resolution failure in a Docker container with different OS. Always include extensions.
🎯 Key Takeaway
Always specify file extensions in ESM imports; use import assertions for JSON; avoid default exports.

Tooling and Ecosystem Support

Modern tools like TypeScript, Webpack, Rollup, and Jest support both CJS and ESM. TypeScript can output both module types via module compiler option. Jest requires configuration to handle ESM (e.g., transform). ESLint's import plugin works with both. For testing, consider using Node's native test runner or Vitest for ESM-first projects. The npm registry hosts packages in both formats, but many popular packages still ship only CJS. Check the exports field in package.json to see what's available. The ecosystem is moving toward ESM, but CJS will remain for years.

tsconfig.jsonJSON
1
2
3
4
5
6
{
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "node"
  }
}
🔥TypeScript and ESM
Set "module": "ESNext" and "moduleResolution": "node" in tsconfig.json for ESM output.
📊 Production Insight
We migrated a TypeScript monorepo to ESM. Jest needed transform: {} and extensionsToTreatAsEsm: ['.ts']. Without it, tests failed with 'Unexpected token' errors.
🎯 Key Takeaway
Most tools support both systems, but ESM requires explicit configuration in some tools like Jest.

The Future: ESM as Default

Node.js is moving toward ESM as the default. Starting from Node 22, the --experimental-default-type flag may become stable. The community is pushing for ESM-first packages. However, CommonJS won't disappear soon due to legacy code. As a developer, you should be comfortable with both. For new projects, choose ESM. For existing CJS projects, consider migration if the benefits (tree-shaking, better static analysis, top-level await) outweigh the migration cost. The Node.js documentation recommends ESM for new projects.

terminalBASH
1
node --experimental-default-type=module index.js
💡Stay updated
Follow Node.js releases. The default type may change in future versions, affecting existing CJS projects.
📊 Production Insight
We delayed migration for too long. When a critical security patch required ESM-only dependencies, we had to rush a migration under pressure. Plan ahead.
🎯 Key Takeaway
ESM is the future default. Start new projects with ESM; plan migration for existing CJS codebases.

Dynamic Imports: import() vs require()

While both import() and require() load modules dynamically, they differ fundamentally in behavior. require() is synchronous and returns the module exports directly. import() is asynchronous and returns a Promise, enabling lazy loading and code splitting. In Node.js, import() works in both CJS and ESM contexts, but require() is only available in CJS. Use import() for conditionally loading modules, especially when the module path is dynamic or when you need to load ESM from CJS. Note that import() always loads the module as ESM, even if the file is CJS, which can cause unexpected behavior if the module uses module.exports.

dynamic-import.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// CJS file
const { createRequire } = require('module');
const require = createRequire(import.meta.url);

async function loadModule() {
  // Dynamic import (ESM)
  const esmModule = await import('./esm-module.mjs');
  console.log(esmModule.default);

  // Dynamic require (CJS only)
  const cjsModule = require('./cjs-module.cjs');
  console.log(cjsModule);
}

loadModule();
Output
ESM default export
{ cjsExport: 'value' }
Try it live
⚠ Don't mix require() and import() in the same file
In ESM files, require() is not available unless you create it via module.createRequire(). In CJS files, import() works but returns a Promise. Avoid using both in the same file to prevent confusion.
📊 Production Insight
In production, import() enables code splitting in bundlers like Webpack, reducing initial bundle size. Use it for route-level lazy loading in server-side apps.
🎯 Key Takeaway
Use import() for dynamic, async loading; require() for synchronous, static loading in CJS.

module.createRequire() — Bridging the Gap

When working in ESM, you lose access to require(). To load CJS modules dynamically, use module.createRequire(). This function creates a require function scoped to the current file's URL. It's essential for gradual migration from CJS to ESM, allowing you to keep legacy CJS dependencies while writing new code in ESM. However, avoid overusing it—it's a crutch, not a long-term solution. Prefer rewriting CJS modules to ESM when possible.

create-require.jsJAVASCRIPT
1
2
3
4
5
6
7
// ESM file
import { createRequire } from 'module';
const require = createRequire(import.meta.url);

const lodash = require('lodash');
const result = lodash.chunk(['a', 'b', 'c', 'd'], 2);
console.log(result);
Output
[ [ 'a', 'b' ], [ 'c', 'd' ] ]
Try it live
💡Use createRequire sparingly
Each call to createRequire creates a new require function. Cache it at the top of your module to avoid overhead.
📊 Production Insight
In production, createRequire can cause subtle issues if the required module has side effects. Prefer static imports for better optimization.
🎯 Key Takeaway
module.createRequire() lets ESM files load CJS modules, but it's a migration tool, not a permanent pattern.

import.meta — Module Metadata in ESM

ESM provides import.meta, an object containing metadata about the current module. The most common property is import.meta.url, which gives the file URL. This replaces __filename and __dirname from CJS. You can derive the directory path using fileURLToPath from the url module. import.meta is also extensible—Node.js adds properties like import.meta.resolve (experimental) for resolving module specifiers. Use import.meta for constructing paths relative to the current module, especially in ESM-only codebases.

import-meta.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
// ESM file
import { fileURLToPath } from 'url';
import { dirname } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

console.log('Current file:', __filename);
console.log('Current dir:', __dirname);
Output
Current file: /home/user/project/import-meta.js
Current dir: /home/user/project
Try it live
🔥No __dirname in ESM
Unlike CJS, ESM doesn't have __dirname or __filename. Always derive them from import.meta.url.
📊 Production Insight
In production, avoid using import.meta.url for runtime path resolution if possible—prefer static paths or environment variables for better testability.
🎯 Key Takeaway
import.meta.url is the ESM replacement for __filename; use fileURLToPath to convert it.

Tree-Shaking Mechanics in ESM vs CJS

Tree-shaking—dead code elimination—works because ESM has static, analyzable imports and exports. Bundlers like Webpack and Rollup can determine which exports are used and remove unused ones. CJS, with its dynamic require() and module.exports, is not statically analyzable, making tree-shaking nearly impossible. For effective tree-shaking, always use named exports (export const foo) rather than default exports, and avoid side effects in modules. Mark your package as "sideEffects": false in package.json to allow aggressive tree-shaking.

tree-shaking.jsJAVASCRIPT
1
2
3
4
5
6
7
// utils.mjs
export const used = () => 'used';
export const unused = () => 'unused'; // Will be tree-shaken

// main.mjs
import { used } from './utils.mjs';
console.log(used());
Output
used
Try it live
💡Enable sideEffects flag
Add "sideEffects": false to your package.json to tell bundlers your package has no side effects, enabling deeper tree-shaking.
📊 Production Insight
In production, tree-shaking can reduce bundle size by 20-50%. Audit your dependencies for CJS modules that block tree-shaking.
🎯 Key Takeaway
Tree-shaking works only with ESM due to static analysis; CJS modules are excluded from dead code elimination.

Package.json Exports Map and Conditional Exports

The exports field in package.json controls how other modules import your package. It replaces the traditional main field and allows subpath exports, conditional exports (based on environment), and preventing access to internal files. Conditional exports let you provide different entry points for different environments (e.g., Node.js vs browser, CJS vs ESM). This is crucial for dual-package hazards where you need to avoid loading the same module twice. Always define both "import" and "require" conditions to support both module systems.

package.jsonJSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
  "name": "my-package",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    },
    "./feature": {
      "import": "./dist/feature.mjs",
      "require": "./dist/feature.cjs"
    }
  },
  "sideEffects": false
}
⚠ Dual-package hazard
If both CJS and ESM versions of the same package are loaded, you may get duplicate instances. Use conditional exports to ensure only one version is loaded.
📊 Production Insight
In production, always test your package with both require() and import() to ensure no duplicate instances or missing exports.
🎯 Key Takeaway
Use exports map with conditional exports to support both CJS and ESM consumers and avoid dual-package hazards.
CommonJS vs ES Modules Key Differences Syntax, exports, and behavior comparison CommonJS (CJS) ES Modules (ESM) Syntax require() and module.exports import and export Export type Single object (module.exports) Named and default exports File extension .js or .cjs .mjs or .js with type:module Circular deps Returns partial exports Throws ReferenceError Tree-shaking Not supported Built-in support THECODEFORGE.IO
thecodeforge.io
Commonjs Vs Es Modules

Loader Hooks: Customizing Module Resolution

Node.js ESM loader hooks allow you to intercept and customize module resolution, loading, and transformation. The three main hooks are resolve (modify specifier resolution), load (transform source code), and transform (deprecated, use load instead). These hooks are powerful for transpilation (e.g., TypeScript), aliasing, or mocking. To use them, create a loader file and pass it via --experimental-loader flag. Note: this API is experimental and may change.

loader.mjsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// loader.mjs
export async function resolve(specifier, context, defaultResolve) {
  if (specifier === 'debug') {
    return { url: 'file:///dev/null' };
  }
  return defaultResolve(specifier, context, defaultResolve);
}

export async function load(url, context, defaultLoad) {
  const result = await defaultLoad(url, context, defaultLoad);
  if (result.format === 'module') {
    // Transform source (e.g., add logging)
    const transformed = `console.log('Loading: ${url}');\n${result.source}`;
    return { format: 'module', source: transformed };
  }
  return result;
}
Output
Loading: file:///path/to/module.mjs
... module output ...
Try it live
🔥Loader hooks are experimental
The loader API is still experimental in Node.js. Use with caution and pin your Node.js version to avoid breaking changes.
📊 Production Insight
In production, avoid custom loaders unless necessary—they add complexity and may break with Node.js updates. Prefer build-time transpilation.
🎯 Key Takeaway
Loader hooks (resolve, load) let you customize ESM module resolution and transformation, useful for transpilation and mocking.
● Production incidentPOST-MORTEMseverity: high

The Silent Undefined: A Circular Dependency Nightmare in CommonJS

Symptom
Random 'TypeError: Cannot read properties of undefined' errors in a critical API endpoint. The error stack trace pointed to a module that exported a function, but the function was undefined at the time of invocation.
Assumption
The team assumed the issue was a race condition in the database layer, as the errors seemed correlated with high traffic. They spent hours optimizing queries and adding connection pooling, but the errors persisted.
Root cause
A circular dependency between two modules (auth.js and user.js) that had been introduced in the latest release. In CommonJS, when module A requires module B, and module B requires module A, the exports of module A are only partially populated at the time module B's require() executes. The function that was undefined was exported from module A but hadn't been assigned yet because module A was still executing when module B tried to use it.
Fix
Refactored the code to break the circular dependency by extracting the shared logic into a third module (common.js) that both modules could import without circularity. Alternatively, we could have used lazy require() inside the function body instead of at the top level, but that would be a band-aid. The proper fix was to eliminate the cycle.
Key lesson
  • Circular dependencies are a design smell; avoid them whenever possible.
  • CommonJS's handling of circular dependencies is fragile and can lead to hard-to-debug undefined values.
  • Use tools like madge or dependency-cruiser to detect circular dependencies in CI.
  • Consider migrating to ES Modules, which handle circular dependencies more gracefully via live bindings.
  • When debugging undefined exports, always check for circular dependencies first.
⚙ Quick Reference
15 commands from this guide
FileCommand / CodePurpose
cjs-example.jsconst fs = require('fs');Module Systems in Node.js
esm-example.mjsexport const greet = (name) => `Hello, ${name}!`;Syntax and Export Differences
package.json{File Extensions and Package Type
cjs-import-esm.cjsasync function loadESM() {Interoperability
circular-cjs.jsconst b = require('./b');Circular Dependencies
tree-shaking-example.jsexport const used = () => 'used';Performance and Tree-Shaking
esm-dirname.mjsconst __filename = fileURLToPath(import.meta.url);Migrating from CJS to ESM
esm-json-import.mjsconsole.log(data);Common Pitfalls and Best Practices
tsconfig.json{Tooling and Ecosystem Support
terminalnode --experimental-default-type=module index.jsThe Future
dynamic-import.jsconst { createRequire } = require('module');Dynamic Imports
create-require.jsconst require = createRequire(import.meta.url);module.createRequire()
import-meta.jsconst __filename = fileURLToPath(import.meta.url);import.meta
tree-shaking.jsexport const used = () => 'used';Tree-Shaking Mechanics in ESM vs CJS
loader.mjsexport async function resolve(specifier, context, defaultResolve) {Loader Hooks

Key takeaways

1
Module System Basics
CommonJS uses require/module.exports (synchronous, runtime); ESM uses import/export (asynchronous, static). Choose ESM for new projects.
2
Interoperability
ESM can import CJS statically; CJS can only import ESM dynamically via import(). Be aware of the asymmetry when mixing systems.
3
Migration Strategy
Migrate incrementally by setting "type": "module", renaming files, and updating imports. Replace __dirname with import.meta.url.
4
Production Readiness
Avoid circular dependencies, always specify file extensions in ESM imports, and test interop with third-party packages. Plan migration early to avoid last-minute rushes.
5
Dynamic imports
Use import() for async, dynamic loading; require() is synchronous and only available in CJS. import() works in both systems but returns a Promise.
6
Tree-shaking
Only ESM supports tree-shaking due to static analysis. Mark your package with "sideEffects": false to enable aggressive dead code elimination.
7
Conditional exports
Use the exports map with "import" and "require" conditions to support both module systems and avoid dual-package hazards.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the key difference in how CommonJS and ES Modules handle module ...
Q02SENIOR
How does the 'this' keyword differ in CommonJS vs ES Modules at the top ...
Q03SENIOR
Explain how circular dependencies are handled differently in CommonJS an...
Q04SENIOR
What is the 'import.meta' object and how does it differ from '__dirname'...
Q05SENIOR
Can you mix CommonJS and ES Modules in the same Node.js project? If so, ...
Q06SENIOR
What is tree-shaking and why is it more effective with ES Modules?
Q01 of 06JUNIOR

What is the key difference in how CommonJS and ES Modules handle module loading?

ANSWER
CommonJS uses synchronous, runtime loading (require() blocks until the module is fully loaded), while ES Modules use asynchronous, static loading (import is parsed before execution and loads dependencies in parallel). This makes ESM more suitable for browser environments and enables tree-shaking.
FAQ · 9 QUESTIONS

Frequently Asked Questions

01
Can I use `require` in an ES module?
02
How do I get `__dirname` in ES modules?
03
Why does my ESM import of a CommonJS module return an object with a `default` property?
04
Can I have both CJS and ESM files in the same project?
05
How do I handle circular dependencies in ESM?
06
What is tree-shaking and how does ESM enable it?
07
Can I use `require()` inside an ESM file?
08
What is the dual-package hazard and how do I avoid it?
09
How do I resolve module paths in ESM without __dirname?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.

Follow
Verified
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
🔥

That's Node.js. Mark it forged?

5 min read · try the examples if you haven't

Previous
Node.js Architecture — Event Loop, libuv, and Async I/O
20 / 47 · Node.js
Next
Debugging Node.js Applications — Inspector, Chrome DevTools, and VS Code