Back to blog

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 headache

Why 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 ESM

Step 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 clearer

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

  1. New projects: Use ESM from the start
  2. Existing projects: Plan gradual migration
  3. Libraries: Consider ESM-only for next major
  4. 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.

Let's go! 🦅

Comments (0)

This article has no comments yet 😢. Be the first! 🚀🦅

Add comments