ESM in 2026: The End of CommonJS and the Modern JavaScript Era
Hello HaWkers, 2026 is officially the year of complete ES Modules (ESM) adoption in the JavaScript ecosystem. With Node.js 20+ allowing ESM imports from CommonJS code, the last major barrier has fallen.
Let's understand this transition and how it affects your code.
What Changed in 2026
Node.js Unified the Worlds
// The big change: CJS can import ESM
// Before (until 2025): This would error
// file.cjs
const esModule = require('./esm-module.mjs'); // ❌ Didn't work
// Now (2026): It works!
// file.cjs
const esModule = require('./esm-module.mjs'); // ✅ Works
// Node.js 20+ (backported) allows this
// This removes the main migration headacheWhy This Is Big
// The historical problem
const esmAdoptionBlocker = {
before: {
problem: 'ESM libraries couldn\'t be used in CJS projects',
symptom: 'ERR_REQUIRE_ESM',
workaround: 'Dynamic import() - ugly and async',
result: 'Many libs maintained dual publish'
},
after: {
solution: 'CJS can require() ESM directly',
impact: 'Libs can be ESM-only without breaking users',
trend: 'New libs are ESM-only by default'
},
timeline: {
node22: 'Feature added',
node20: 'Backported',
node18: 'Backported (LTS)',
adoption: 'All non-EOL Node.js support it'
}
};
CommonJS vs ESM: The Differences
Syntax
// CommonJS (the old way)
// Export
module.exports = { foo, bar };
// or
exports.foo = foo;
// Import
const { foo, bar } = require('./module');
const fs = require('fs');
// ESM (the modern way)
// Export
export { foo, bar };
export default myFunction;
// Import
import { foo, bar } from './module.js';
import fs from 'node:fs';Important Differences
// 1. Loading timing
// CommonJS: Synchronous, at runtime
const a = require('./a'); // Executes now
if (condition) {
const b = require('./b'); // Executes conditionally
}
// ESM: Asynchronous, before runtime
import a from './a.js'; // Loaded before code executes
// conditional import requires dynamic import
const b = condition ? await import('./b.js') : null;
// 2. __dirname and __filename
// CommonJS: Available automatically
console.log(__dirname); // /path/to/folder
console.log(__filename); // /path/to/folder/file.js
// ESM: Need to create
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// 3. JSON imports
// CommonJS
const config = require('./config.json'); // Works
// ESM
import config from './config.json' with { type: 'json' }; // New syntax
// or
import { readFileSync } from 'node:fs';
const config = JSON.parse(readFileSync('./config.json', 'utf8'));
// 4. Top-level await
// CommonJS: Not allowed
// await fetch(...); // ❌ SyntaxError
// ESM: Works!
const data = await fetch('...'); // ✅ Top-level await
export const result = await processData(data);
Why Migrate to ESM
Technical Benefits
// ESM advantages
const esmBenefits = {
// 1. Tree shaking
treeShaking: {
what: 'Bundlers remove unused code',
requirement: 'Static imports (ESM)',
impact: 'Smaller bundles',
example: `
// You import only what you use
import { debounce } from 'lodash-es';
// Bundler includes only debounce, not entire lodash
`
},
// 2. Static analysis
staticAnalysis: {
what: 'Tools understand imports before execution',
benefits: [
'Better IDE autocomplete',
'Broken import detection',
'Safer refactoring'
]
},
// 3. Browser-native
browserNative: {
what: 'ESM works in browser without bundler',
use: 'Local development, small apps',
syntax: '<script type="module" src="app.js"></script>'
},
// 4. Top-level await
topLevelAwait: {
what: 'await outside async functions',
useful: 'Module initialization',
example: 'export const db = await connectDB();'
},
// 5. Future-proof
futureProof: {
what: 'ESM is the official standard',
cjs: 'CommonJS is Node.js-specific',
trend: 'Ecosystem migrating to ESM'
}
};The Ecosystem Is Changing
// Ecosystem state in 2026
const ecosystem2026 = {
// Libs that are ESM-only now
esmOnly: [
'chalk 5+',
'node-fetch 3+',
'execa 6+',
'got 12+',
'globby 13+',
'p-limit 4+',
'nanoid 4+',
// And many others...
],
// Frameworks that prefer ESM
frameworks: {
vite: 'ESM-first',
nuxt3: 'ESM by default',
astro: 'ESM by default',
sveltekit: 'ESM by default'
},
// What's still CommonJS
stillCJS: {
express: 'Works in both',
lodash: 'Has lodash-es for ESM',
moment: 'Use dayjs or date-fns',
legacy: 'Unmaintained projects'
}
};
How to Migrate to ESM
Step 1: Configure package.json
// Add "type": "module"
// package.json
{
"name": "my-project",
"version": "1.0.0",
"type": "module", // ← This makes .js treated as ESM
"main": "index.js",
"exports": {
".": "./index.js",
"./utils": "./utils/index.js"
}
}
// With "type": "module":
// - .js files are ESM
// - .cjs files are CommonJS (if needed)
// Without "type": "module" (or "type": "commonjs"):
// - .js files are CommonJS
// - .mjs files are ESMStep 2: Update Imports
// Required transformations
// From:
const path = require('path');
const { readFile } = require('fs/promises');
const myModule = require('./my-module');
// To:
import path from 'node:path';
import { readFile } from 'node:fs/promises';
import myModule from './my-module.js'; // Note the .js!
// IMPORTANT: ESM requires file extension
import utils from './utils'; // ❌ Doesn't work
import utils from './utils.js'; // ✅ Works
// For Node modules, use node: prefix
import fs from 'node:fs'; // ✅ Recommended
import fs from 'fs'; // Works, but node: is clearerStep 3: Update Exports
// Export transformations
// From:
module.exports = myFunction;
module.exports = { foo, bar };
exports.something = something;
// To:
export default myFunction;
export { foo, bar };
export const something = something;
// Mixed exports
export default mainFunction;
export { helper1, helper2 };Step 4: Fix __dirname/__filename
// Create a helper or add at top of files that need it
// esm-utils.js
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
export function getDirname(importMetaUrl) {
return dirname(fileURLToPath(importMetaUrl));
}
// usage in any file:
import { getDirname } from './esm-utils.js';
const __dirname = getDirname(import.meta.url);
// Or inline (more common):
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
Step 5: Update JSON Imports
// Option 1: Import assertion (Node 20+)
import config from './config.json' with { type: 'json' };
// Option 2: Manual reading (more compatible)
import { readFileSync } from 'node:fs';
const config = JSON.parse(
readFileSync(new URL('./config.json', import.meta.url), 'utf8')
);
// Option 3: createRequire (useful hack)
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const config = require('./config.json');Migration Strategies
Gradual Migration
// For large projects, migrate gradually
const migrationStrategy = {
// Phase 1: Prepare
phase1: {
actions: [
'Identify ESM-only dependencies',
'Check Node version (18+)',
'Configure ESLint for ESM'
]
},
// Phase 2: Dual mode
phase2: {
how: 'Use .mjs for new ESM files',
keep: 'Existing .js files as CJS',
benefit: 'Incremental migration'
},
// Phase 3: Flip the switch
phase3: {
action: 'Add "type": "module"',
rename: 'Remaining CJS files to .cjs',
test: 'Run all tests'
},
// Phase 4: Cleanup
phase4: {
actions: [
'Rename .mjs to .js',
'Remove workarounds',
'Update documentation'
]
}
};For Libraries
// If you maintain an npm lib
const libraryMigration = {
// Option 1: ESM-only (recommended in 2026)
esmOnly: {
packageJson: {
"type": "module",
"exports": {
".": "./dist/index.js"
}
},
pros: 'Simple, modern',
cons: 'Breaks CJS users on Node < 20',
when: 'New lib or major version'
},
// Option 2: Dual package (CJS + ESM)
dual: {
packageJson: {
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
},
pros: 'Maximum compatibility',
cons: 'More complex build, dual package hazard',
when: 'Popular lib with legacy users'
}
};
Tools That Help
Codemods and Linters
// Migration tools
const migrationTools = {
// ESLint
eslint: {
plugin: 'eslint-plugin-import',
rules: {
'import/extensions': ['error', 'always'],
'import/no-commonjs': 'error'
}
},
// Codemod
codemod: {
tool: 'jscodeshift',
transforms: 'cjs-to-esm transforms available',
command: 'npx cjs-to-esm ./src/**/*.js'
},
// TypeScript
typescript: {
config: {
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "NodeNext"
}
},
benefit: 'TS generates ESM automatically'
},
// Bundlers
bundlers: {
vite: 'Native ESM',
rollup: 'ESM-first',
esbuild: 'Excellent support'
}
};Common Errors and Solutions
Troubleshooting
// Common migration errors
const commonErrors = {
// Error 1: Cannot use import statement outside a module
error1: {
message: 'Cannot use import statement outside a module',
cause: 'File treated as CJS',
solutions: [
'Add "type": "module" in package.json',
'Use .mjs extension',
'Check if there\'s no parent package.json'
]
},
// Error 2: ERR_MODULE_NOT_FOUND
error2: {
message: 'ERR_MODULE_NOT_FOUND',
cause: 'Missing extension in import',
solution: 'import x from "./file.js" (not "./file")'
},
// Error 3: __dirname is not defined
error3: {
message: '__dirname is not defined',
cause: '__dirname doesn\'t exist in ESM',
solution: 'Use import.meta.url + fileURLToPath'
},
// Error 4: require is not defined
error4: {
message: 'require is not defined',
cause: 'Using require in ESM module',
solution: 'Use import or createRequire'
},
// Error 5: JSON import doesn\'t work
error5: {
message: 'Unknown file extension ".json"',
cause: 'JSON needs assertion',
solution: 'import x from "./x.json" with { type: "json" }'
}
};
Conclusion
2026 marks the turning point for ESM in JavaScript. With Node.js removing the last major barrier (CJS importing ESM), there's no more excuse not to migrate.
Recommended actions:
- New projects: Use ESM from the start
- Existing projects: Plan gradual migration
- Libraries: Consider ESM-only for next major
- Update Node.js: 20+ for best experience
CommonJS served well for 15 years, but its time is coming to an end. ESM is the future - and the future is now.
To understand more about the modern JavaScript ecosystem, read: VoidZero 2026.

