1. Introduction
As applications grow, keeping all code in one file becomes unmanageable. Modules let you split code into separate files, each with its own private scope, and explicitly share functionality between them using export and import. ES6 introduced a native module system (ES Modules, or ESM) that is now supported by all modern browsers and Node.js. Before ESM, Node.js relied on CommonJS (require/module.exports), which is still common in older codebases and some npm packages.
Cricket analogy: Like splitting a huge tour handbook into separate chapters per department — batting, bowling, fielding — each with its own private notes, sharing only the official drills via an explicit export list, versus the old single shared master binder (CommonJS) still used by older touring squads.
2. Syntax
// math.js -- named exports
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
// math.js -- default export (one per module)
export default function multiply(a, b) {
return a * b;
}
// app.js -- importing
import multiply, { PI, add } from './math.js';
import * as MathUtils from './math.js';
// package.json needs: { "type": "module" } (Node.js)
// or use a <script type="module"> tag in the browser3. Explanation
A module can have any number of *named exports* (exported with export const, export function, export class, or a trailing export { a, b }) plus at most one *default export*. Named exports must be imported using the exact same name (optionally renamed with as), while a default export can be imported under any name you choose. import * as Name gathers every named export into a single namespace object.
Cricket analogy: Like a squad sheet with many named specialist roles (opener, keeper, spinner — named exports) imported by their exact title, plus one designated captain (default export) who can be called anything you like, and a full squad roster gathered under one name (import * as Squad).
ES modules are statically analyzed: import/export declarations must appear at the top level of a file (not inside an if or a function), and their bindings are resolved at parse time, before any code runs. This static structure is what allows bundlers like Webpack, Rollup, and esbuild to perform *tree-shaking* — removing exports that are never imported anywhere. CommonJS, by contrast, uses require() calls that execute at runtime and can appear anywhere (even conditionally), so it cannot be statically analyzed the same way.
Cricket analogy: Like a tour selector finalizing the entire squad list before the season starts, letting the board trim unused reserves in advance (tree-shaking), versus an old-school system where players could be called up mid-match on demand (runtime require) making advance trimming impossible.
Gotcha — ES modules vs CommonJS is not just a syntax difference: import/export are static and top-level only, evaluated asynchronously, and always run in strict mode. require/module.exports are dynamic, can run anywhere at runtime (including inside conditionals or functions), and execute synchronously. Mixing the two systems in the same project (e.g. require()-ing an ESM-only package) is a frequent source of 'Cannot use import statement outside a module' or 'require() of ES Module not supported' errors.
Because import is hoisted and static, this is invalid: if (condition) { import { x } from './x.js'; } — you must use the dynamic import('./x.js') function (which returns a Promise) if you need conditional or lazy loading.
4. Example
// geometry.js
export const SHAPE = 'circle';
export function area(radius) {
return (Math.PI * radius * radius).toFixed(2);
}
export default function describe(radius) {
return `${SHAPE} with area ${area(radius)}`;
}
// main.js
import describe, { SHAPE, area } from './geometry.js';
console.log(SHAPE);
console.log(area(2));
console.log(describe(2));5. Output
circle
12.57
circle with area 12.576. Key Takeaways
- export exposes named bindings; export default exposes one primary value per module.
- import brings named exports in by exact name and the default export under any chosen name.
- import/export declarations must be at the top level and are statically analyzed at parse time.
- ES Modules run in strict mode automatically and are evaluated asynchronously.
- CommonJS (require/module.exports) is dynamic, synchronous, and resolved at runtime — mixing it with ESM causes common errors.
- Use the dynamic import() function (returns a Promise) for conditional or lazy-loaded modules.
Practice what you learned
1. How many default exports can a single ES module have?
2. Why can't you write `if (x) { import { y } from './y.js'; }`?
3. What is a key functional difference between ESM import and CommonJS require?
4. How do you import everything a module exports as named exports into one object?
5. What does the static, analyzable structure of ES modules enable that CommonJS generally cannot support as well?
Was this page helpful?
You May Also Like
Functions in JavaScript
Learn how to declare, invoke, and understand the hoisting behavior of JavaScript functions.
Closures in JavaScript
Understand how closures let inner functions remember and access variables from their outer scope, and how loop variable capture can trip you up.
Classes in JavaScript
How ES6 class syntax provides a cleaner, structured way to create objects and implement object-oriented patterns on top of JavaScript's prototype system.
Common JavaScript Interview Questions
A curated set of frequently asked JavaScript interview questions covering fundamentals, closures, the event loop, and ES6+ features.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics