Historical Context & Motivation
When JavaScript was first created in 1995, it was designed to add small bits of interactivity to web pages โ things like form validation or simple animations. A typical script might be only a dozen lines long, and everything lived inside a single file. There was no built-in way to split code across multiple files and share functionality between them, because nobody imagined that JavaScript programs would ever grow beyond a few hundred lines.
Fast-forward to today, and JavaScript applications regularly contain thousands โ even millions โ of lines of code. Without a strategy for organizing all that code, developers would face a nightmare of naming collisions (two variables accidentally sharing the same name), tangled dependencies (file A relies on file B which relies on file C), and code that is nearly impossible to debug. The solution to this chaos is the module system โ a standardized way to break a program into smaller, self-contained files that can export functionality and import what they need from other files.
require() and module.exports.import and export keywords as part of the official language standard.This history leaves us with two major module systems that you will encounter in the JavaScript ecosystem. Understanding how they differ โ and when to use each โ is a foundational skill for any JavaScript developer today.
Core Principles of Modules
Before diving into the specific syntax of CommonJS or ES Modules, it helps to understand the core ideas that any module system is trying to achieve. These principles explain why modules exist and what problems they solve.
Encapsulation
Reusability
Dependency Management
Maintainability
How Modules Connect โ Visual Overview
The diagram below shows the fundamental pattern shared by both CommonJS and ES Modules. A source module defines private code and selectively exposes pieces through exports. A consuming module then imports only the pieces it needs, without gaining access to the private internals.
greet and PI). Module B imports those exports but cannot access the private variables secret or counter.This export-import pattern is the beating heart of both CommonJS and ES Modules. The difference between the two systems lies in how you write the export and import statements, when the code is loaded, and where each system is most commonly used. We will explore those differences next.
How Each Module System Works
CommonJS (CJS)
CommonJS was created for Node.js and is the default module system on the server side. It uses two key pieces of syntax: require() to import a module and module.exports (or the shorthand exports) to export values. CommonJS loads modules synchronously, meaning the program waits for the file to be fully read and executed before moving on. This works fine on a server where files are on the local disk, but it can cause delays in a browser that has to download files over the internet.
module.exports = { greet, PI }; โข Importing: const { greet, PI } = require('./moduleA'); The require() call can appear anywhere in the file, even inside an if block โ it is a regular function call.ES Modules (ESM)
ES Modules are the official standard built into the JavaScript language since ES6 (2015). They use the keywords import and export. Unlike CommonJS, ES Modules are loaded asynchronously, which makes them ideal for the browser. The import and export statements must appear at the top level of the file โ you cannot place them inside a function or an if block. This restriction lets JavaScript engines analyze the module structure before running any code, enabling powerful optimizations like tree shaking (removing unused exports from the final bundle).
export function greet() { ... } โข Default export: export default function greet() { ... } โข Named import: import { greet } from './moduleA.js'; โข Default import: import greet from './moduleA.js';A key conceptual difference is static vs. dynamic analysis. Because CommonJS uses a regular function (require()), JavaScript cannot know what a module exports until the code actually runs. ES Module import/export statements are statically analyzable โ the engine can map out the entire dependency graph before any code executes.
Side-by-Side Comparison
The table below summarizes the key differences between CommonJS and ES Modules. Refer back to this chart whenever you need a quick refresher.
| Feature | CommonJS (CJS) | ES Modules (ESM) |
|---|---|---|
| Import syntax | require('./file') | import x from './file' |
| Export syntax | module.exports = ... | export / export default |
| Loading | Synchronous (blocking) | Asynchronous (non-blocking) |
| Analysis | Dynamic โ resolved at runtime | Static โ resolved before execution |
| Tree shaking | Not natively supported | Supported โ unused exports removed |
| File extension | .js (default in Node) | .mjs or .js with "type": "module" |
| Primary environment | Node.js (server) | Browsers & modern Node.js |
Notice the label "live bindings" on the ES Modules side. When you import a value with ESM, you get a live reference to the original variable โ if the source module updates it, your import automatically reflects the change. In CommonJS, require() returns a copy of the exported value at the moment of import. This subtle difference rarely matters for beginners, but it becomes significant in advanced patterns.
Worked Example โ Building a Utility Module
Let's walk through creating a small utility module in both CommonJS and ES Module syntax. Imagine you are building a calculator app and you want to keep your math functions in a separate file so that any part of your project can reuse them.
module.exports. Anything not exported stays private.function add(a, b) { return a + b; } function subtract(a, b) { return a - b; } module.exports = { add, subtract };require() with the relative path to the module file. Destructure the object to grab only the functions you need.const { add, subtract } = require('./math');math.js remain inaccessible.console.log(add(3, 5)); // 8 console.log(subtract(10, 4)); // 6export keyword directly in front of each declaration you want to make available.export function add(a, b) { return a + b; } export function subtract(a, b) { return a - b; }import keyword at the top of the file. Note the curly braces for named imports and the file extension in the path.import { add, subtract } from './math.mjs';console.log(add(3, 5)); // 8 console.log(subtract(10, 4)); // 6require() / module.exports and runs synchronously. ES Modules use import / export and run asynchronously. Think of it like texting (async โ you send and keep doing other things) versus a phone call (sync โ you wait until the other person finishes talking).Strengths & Limitations
Neither module system is universally "better" โ each has strengths and trade-offs depending on the environment and use case. The table below helps you decide which one fits a given situation.
| Criteria | CommonJS Strength? | ES Modules Strength? |
|---|---|---|
| Browser support | โ Not natively supported in browsers | โ Natively supported in all modern browsers |
| Node.js ecosystem | โ Vast existing package library uses CJS | โ ๏ธ Growing adoption; some older packages CJS-only |
| Tree shaking | โ Not possible (dynamic analysis) | โ Bundlers can remove unused exports |
| Conditional imports | โ require() works inside if blocks | โ ๏ธ Needs dynamic import() for conditional loading |
| Future direction | โ ๏ธ Legacy; maintained but not expanding | โ The official JavaScript standard going forward |
Connection to Advanced Tooling
Understanding module systems at a conceptual level opens the door to more advanced tools in the JavaScript ecosystem. Tools called bundlers โ like Webpack, Vite, and Rollup โ take your many module files and combine them into one or a few optimized files for deployment. Bundlers rely heavily on the static analysis capabilities of ES Modules to perform tree shaking, code splitting, and lazy loading.
| Concept | What You Know Now | Where It Leads |
|---|---|---|
| Modules | Splitting code into files with import/export | Bundlers (Webpack, Vite) that combine modules for production |
| Static imports | import/export at the top of the file | Dynamic import() for lazy loading and code splitting |
| npm packages | Third-party modules installed via npm | Package management, lockfiles, and monorepos |
| Encapsulation | Private scope within each module | Design patterns like the Revealing Module Pattern, classes |
As you continue learning, you will also encounter TypeScript, which adds type checking on top of ES Module syntax, and Deno, a newer JavaScript runtime that uses ES Modules exclusively โ it dropped CommonJS entirely. The trend across the ecosystem is clearly moving toward ES Modules as the universal standard.
Practice Problems
helpers.js that contains a function capitalize. Write the one line of code needed to export it using CommonJS, and then write the one line needed to import it in another file called app.js.const axios = require('axios'); and module.exports = { fetchData };. Explain one advantage that the ES Module version gains over the CommonJS version.require() to load modules. Explain why this will not work directly in the browser and recommend an alternative approach.import statements to be at the top level (not inside functions or conditionals), while CommonJS allows require() anywhere. Argue why the ES Module restriction is actually a feature, not a limitation. Then describe one scenario where the flexibility of CommonJS require() is genuinely useful.Lesson Summary
JavaScript module systems solve the problem of organizing large codebases by splitting code into self-contained files with private scopes. The two major systems are CommonJS โ which uses require() and module.exports, loads synchronously, and is the traditional Node.js standard โ and ES Modules โ which uses import and export keywords, loads asynchronously, and is the official JavaScript language standard.
Both systems achieve encapsulation, reusability, and dependency management. ES Modules offer the additional advantage of static analysis, enabling tree shaking and other optimizations. For new projects, ES Modules are the recommended choice, but fluency in CommonJS is essential because it remains widespread across the npm ecosystem and existing Node.js codebases.