Suggest an editImprove this articleRefine the answer for “JavaScript modules: import/export, CommonJS vs es modules”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**JavaScript modules** are files with their own scope that share code through imports and exports. CommonJS (`require`/`module.exports`) loads synchronously at runtime. ES Modules (`import`/`export`) parse statically and support tree-shaking and top-level `await`. ```javascript export const PI = 3.14159; // ESM named export module.exports = { PI }; // CommonJS equivalent ``` **Key point:** ESM is static (parse-time), CommonJS is dynamic (runtime). Use ESM for all new code.Shown above the full answer for quick recall.Answer (EN)Image**JavaScript modules** split code into separate files, each with its own scope. Two systems exist: CommonJS (`require`/`module.exports`), the original Node.js default, and ES Modules (`import`/`export`), the current web standard. ## Theory ### TL;DR - Analogy: modules are like restaurant kitchens. Each exports dishes through a menu (exports), and other kitchens import what they need without sharing raw ingredients. No global scope pollution. - CommonJS loads synchronously at runtime. ES Modules parse statically at load time. - ESM enables tree-shaking; CommonJS does not. - Node 14+ supports both. Browsers only support ESM natively. - Decision rule: use ESM for all new code. CommonJS only for legacy Node codebases. ### Quick example Same math utilities, two module formats: ```javascript // CommonJS const PI = 3.14159; function add(a, b) { return a + b; } module.exports = { PI, add }; const math = require('./math.cjs'); console.log(math.add(math.PI, 1)); // 4.14159 // ES Modules export const PI = 3.14159; export function add(a, b) { return a + b; } import { PI, add } from './math.mjs'; console.log(add(PI, 1)); // 4.14159 ``` ESM lets you import exactly what you need by name. CommonJS returns the whole `module.exports` object and you destructure it yourself. ### Key difference ES Modules build a static dependency graph at parse time. Bundlers like Webpack and Rollup read that graph before any code runs, so they drop unused exports (tree-shaking). CommonJS resolves `require()` calls at runtime, meaning the bundler cannot know ahead of time what code is actually used. That single difference explains why migrating a library from CJS to ESM can shrink bundle sizes noticeably. ESM also supports top-level `await`; CommonJS cannot, because synchronous loading and async simply do not mix. ### When to use - Browser project or Vite/Webpack: ESM. Native browser support, tree-shaking, no extra config. - Node.js 14+: ESM with `.mjs` extension or `"type": "module"` in `package.json`. - Node.js older than 14, or code that needs `require(variable)` at runtime: CommonJS. - Bridging the two: dynamic `import()` works from both CommonJS and ESM files. ### Comparison table | Feature | ES Modules | CommonJS | |---|---|---| | Syntax | `import` / `export` | `require()` / `module.exports` | | Loading | Static, parse-time | Synchronous, runtime | | Browser support | Native | Needs bundler | | Tree-shaking | Yes | No | | Top-level `await` | Yes | No | | Default export | `export default foo` | `module.exports = foo` | | Dynamic imports | `await import(variable)` | `require(variable)` directly | | Node.js default | No (needs `.mjs` or config) | Yes (pre-v14) | | When to use | React 18+, Node 20+, all new code | Legacy Express 4.x, Node < 14 | ### How the engine handles this V8 parses ESM at load time, builds a static dependency graph, and links export bindings before evaluation. Node.js checks the `"type"` field in `package.json` first, then the file extension: `.mjs` forces ESM, `.cjs` forces CommonJS. CommonJS uses `require.cache` to store loaded modules, so calling `require('./math')` twice returns the same cached `module.exports` object without re-executing the file. ### Common mistakes **Forgetting the `.js` extension in Node ESM:** ```javascript import { PI } from './math'; // Error: Cannot find module import { PI } from './math.js'; // Works ``` Node ESM requires explicit file extensions. CommonJS infers them. This catches almost everyone migrating from CJS. **Using `require` inside an ESM file:** ```javascript // .mjs file, or package.json with "type": "module" const fs = require('fs'); // SyntaxError: require is not defined in ES module scope import fs from 'node:fs'; // Correct ``` **Multiple default exports:** ```javascript export default foo; export default bar; // SyntaxError: only one default export per module ``` Use named exports when you need both. **Exporting before defining in CommonJS:** ```javascript module.exports.add = add; // ReferenceError: add is not defined function add(a, b) { return a + b; } // define first module.exports = { add }; // then export ``` ESM handles this through hoisting automatically. **Expecting tree-shaking from CommonJS in a bundler:** ```javascript // Bundler cannot remove unusedFn here module.exports.unusedFn = function() {}; module.exports.usedFn = function() {}; // Switch to ESM named exports for dead code elimination ``` ### Real-world usage - React 18+: `import React from 'react'` - tree-shaken, ESM standard. - Node 20 + Express 5: `"type": "module"` in `package.json`, `import express from 'express'`. - Vite (SvelteKit, Next.js): native ESM, dynamic `import()` for route-level code splitting. - Webpack 5: ESM output by default, handles CommonJS inputs. - Deno and Bun: ESM-only runtimes, zero config required. ### Follow-up questions **Q:** How does tree-shaking work in ESM but not CommonJS? **A:** ESM exports are static names known at parse time. Bundlers trace which names are actually imported and drop the rest. In CommonJS, `module.exports` is a plain object assigned at runtime, so the bundler cannot determine what is used without executing the code. **Q:** What happens with circular module dependencies? **A:** ESM partially resolves export bindings before evaluation, so most circular imports work. CommonJS may return an empty `{}` for the not-yet-finished module, which causes runtime errors that are difficult to trace. **Q:** How do you enable ESM in Node.js? **A:** Add `"type": "module"` to `package.json` or use the `.mjs` extension. To force CommonJS explicitly, use `.cjs`. **Q:** How does dynamic `import()` work? **A:** `const mod = await import('./math.js')` returns a Promise of the module namespace object. It works inside both ESM and CommonJS files, which makes it the standard way to bridge mixed codebases. **Q:** Can browsers use ESM without a bundler? **A:** Yes. `<script type="module" src="./main.js"></script>` works in all modern browsers. You need relative paths with explicit extensions (`./math.js`), not bare specifiers (`math`). **Senior Q:** Walk through what V8 actually does when it encounters an `import` statement, and why that enables top-level `await`. **A:** V8 creates a ModuleRecord at parse time, fetches and resolves all dependencies asynchronously (building a full graph), then links each exported name to a live binding before any module code runs. Because the entire linking phase is async, the engine can suspend evaluation waiting for `await` without blocking the thread. CommonJS `require()` is a synchronous function call - it runs the file inline and returns whatever `module.exports` holds at that moment. No async phase means no top-level `await`. ## Examples ### Named and default exports ```javascript // userService.js export const DEFAULT_ROLE = 'viewer'; // named export export function createUser(name, role = DEFAULT_ROLE) { return { name, role, id: Date.now() }; } export default class UserService { // default export async getUser(id) { return { id, name: 'Alice', role: 'admin' }; } } ``` ```javascript // main.js import UserService, { createUser, DEFAULT_ROLE } from './userService.js'; const service = new UserService(); const user = await service.getUser(1); // { id: 1, name: 'Alice', role: 'admin' } const newUser = createUser('Bob'); // { name: 'Bob', role: 'viewer', id: 1718000000000 } ``` Named and default exports coexist in one file. The default is for the module's main thing; named exports are for utilities alongside it. ### Dynamic imports and barrel files ```javascript // services/index.js (barrel file) export { default as UserService } from './userService.js'; export { default as ProductService } from './productService.js'; export * from './utils.js'; // app.js - cleaner import paths import { UserService, ProductService } from './services/index.js'; ``` Barrel files simplify import paths across a codebase. Vite and Webpack 5 tree-shake through them correctly. Older bundlers may pull in more than expected, so check bundle output if you add many of them. Dynamic imports for on-demand loading: ```javascript // Load a heavy module only when actually needed async function handleExport(data) { const { generatePDF } = await import('./pdfGenerator.js'); return generatePDF(data); } // React lazy loading - same mechanism const ReportPage = React.lazy(() => import('./pages/ReportPage.js')); ``` ### Circular dependency behavior This separates candidates who have read the module spec from those who only know the syntax. ```javascript // a.mjs import { fnB } from './b.mjs'; export function fnA() { return fnB(); } // b.mjs import { fnA } from './a.mjs'; export function fnB() { return 'B called'; } // main.mjs import { fnA } from './a.mjs'; console.log(fnA()); // 'B called' - ESM handles this correctly ``` ESM resolves this because exports are live bindings. By the time `fnA` actually calls `fnB`, the binding is already set up. The CommonJS equivalent breaks: `require('./b')` during loading of `a` returns `{}` because `b` has not finished yet. I have seen this produce silent `undefined is not a function` errors in Express apps where two service files imported each other - the kind of bug that takes an afternoon to find.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.