ES Modules vs CommonJS: import vs require in JavaScript
Basics4 minInnovialabs editorial team
In short
CommonJS (require) is the older synchronous module system of Node.js, ES Modules (import) is the official language standard; Node.js picks the system by file extension and the type field in package.json.
The difference in short
JavaScript has two module systems:
- CommonJS (CJS) —
require()andmodule.exports. Node.js had it long before the standard existed, and it still lives in older packages and configs. - ES Modules (ESM) —
importandexport. The official language standard, supported by browsers and Node.js.
For new code choose ESM. You need to understand CommonJS to read existing code and fix compatibility errors.
// CommonJS
const fs = require('fs');
function sum(a, b) { return a + b; }
module.exports = { sum };
// ES Modules
import fs from 'node:fs';
export function sum(a, b) { return a + b; }
How they really differ
| CommonJS | ES Modules | |
|---|---|---|
| Syntax | require, module.exports | import, export |
| Loading | synchronous, at runtime | statically analyzed before running |
| Conditional import | require anywhere | import() — dynamic, returns a Promise |
| Top-level await | no | yes |
__dirname, __filename | available | not available, use import.meta |
| Strict mode | optional | always on |
| Tree shaking | works poorly | works |
The key practical consequence: ESM imports are static. A bundler sees in advance what is imported from where and can drop unused code. With require that is harder, because the path can be computed at runtime.
How Node.js decides which system a file uses
Node.js looks at the extension and package.json:
.mjs— always ESM..cjs— always CommonJS..js— depends on thetypefield in the nearest package.json:"module"means ESM,"commonjs"or no field means CommonJS.
To switch a project to ESM:
{
"type": "module"
}
Then keep in mind:
- Relative imports need file extensions:
import { sum } from './math.js'. __dirnameis gone. Useimport.meta.urlwithfileURLToPath, orimport.meta.dirnamein recent Node.js versions.- Rename tool configs that still expect CommonJS to
.cjs.
Where the typical errors come from
"Cannot use import statement outside a module" — a file with import is being run as CommonJS. Causes:
- package.json has no
"type": "module"and the file ends in.js; - in the browser the script is loaded without
type="module"; - a test runner or older tool is not configured for ESM.
Fix: add "type": "module", rename the file to .mjs, or use <script type="module">.
"require is not defined in ES module scope" — the reverse: an ESM file calls require. Switch to import or use createRequire from node:module.
ERR_REQUIRE_ESM — CommonJS code tries to require a package shipped only as ESM. Newer Node.js versions can require() ES modules that have no top-level await; on older ones only dynamic import() or moving to ESM helps.
"Cannot find module './utils'" in ESM — the file extension is missing from the path.
Interop rules
ESM importing CommonJS — almost always works:
import pkg from './legacy.cjs'; // the whole module.exports
import { helper } from './legacy.cjs'; // works if Node.js can detect exports statically
If a named import is not found, import the default object and destructure it.
CommonJS loading ESM — trickier:
// in a CommonJS file
async function main() {
const { sum } = await import('./math.mjs');
console.log(sum(2, 3));
}
main();
Dynamic import() always works in CommonJS, but it is asynchronous.
How to choose for your project
- New application — ESM with
"type": "module". - Next.js, Vite and other modern frameworks — write
import/export, the bundler handles the rest. - Older Node.js service on CommonJS — migrate gradually: new files as
.mjs, or switch entirely once you have tests. - npm library — ESM as the main format; if older consumers matter, also ship a CommonJS build through the
exportsfield.