Skip to main content

Command Palette

Search for a command to run...

CommonJs Vs EcmaScript Modules

Basic to advance guide to understand CJS and ESM

Published
8 min read
CommonJs Vs EcmaScript Modules

Okay, so you've probably seen words like import, export, and require floating around in JavaScript code and thought what even is this?

Don't worry. By the end of this, it'll click.

Let's start from the very beginning.

Imagine a Classroom With No Desks

Picture a classroom where every student just throws their stuff on the floor. Books, bags, lunch boxes all mixed together in one giant pile. Now imagine trying to find your pencil in that mess.

That's literally what early JavaScript was like.

Every file you wrote dumped its variables into one shared space called the global scope. And here's where it got ugly — if two files used the same variable name, the second one would just wipe out the first. No warning. No error. Just silent, confusing chaos.

Say you had two files:

Example:

https://codepen.io/editor/Satpalsinh-Rana/pen/019cfcc2-89a6-780a-bc59-8ecb8f458177

If both are loaded on the same page, price is now 0. Your cart logic just got quietly destroyed by your checkout file. Fun times.

The Fix: Give Every File Its Own Room

This is exactly what modules do. A module is just a JavaScript file that keeps its variables to itself. Nothing leaks out accidentally. If you want another file to use something, you have to deliberately export it. And the other file has to deliberately import it.

It's like going from that messy floor to everyone having their own desk with a locked drawer. You only share what you choose to share.

Simple idea. Huge difference.

Two Ways JavaScript Does This

There are two main module systems you'll come across. Don't let that scare you — they do the same thing, just with slightly different styles.

CommonJS — The Old School Way

This one was invented for Node.js (JavaScript on the server). You'll see it a lot in older projects and tutorials.

It uses two things:

  • module.exports — to send something out

  • require() — to bring something in

Sending code out:

// greet.js
const sayHello = (name) => {
  console.log("Hello, " + name + "!");
};

module.exports = { sayHello };

Bringing it in:

const { sayHello } = require('./greet.js');
sayHello("Riya"); // Hello, Riya!

That's really it. You wrap your stuff in module.exports, and you grab it with require().

You might see people write exports.sayHello = ... as a shortcut. It mostly works, but the moment you do exports = { sayHello }, it breaks. Stick to module.exports when you're starting out. Way less confusing.

ES Modules — The Modern Way

In 2015, JavaScript introduced a cleaner, more modern system called ES Modules. This is what most tutorials and frameworks use today, and it's what you should focus on learning.

Instead of require and module.exports, you use import and export. Much more readable.

Named Exports sharing multiple things

// math.js
export const add = (a, b) => a + b;
export const PI = 3.14;
// main.js
import { add, PI } from './math.js';
console.log(add(2, PI)); // 5.14

Notice the curly braces {} in the import? That tells JavaScript exactly which piece you want.

Default Export — when a file has one main thing

// User.js
export default class User {
  constructor(name) {
    this.name = name;
  }
}
// main.js
import User from './User.js'; // No curly braces needed!
const me = new User("Arjun");

No curly braces here because there's only one default thing JavaScript already knows what you mean.

Seeing both together

If you've ever used React, you've already seen this in action:

import React, { useState, useEffect } from 'react';
  • React → default export (no braces)

  • useState, useEffect → named exports (inside braces)

Once you get that distinction, a lot of code starts making sense.

So What Should You Actually Use?

If you're just starting out go with ES Modules. It's cleaner to read, it's the modern standard, and every major framework (React, Vue, Svelte) uses it.

You'll run into CommonJS eventually, especially in older Node.js code. But now that you understand the why behind modules, picking up either syntax is just a matter of remembering a few keywords.

Okay, You've Got the Basics now Let's Go Deeper

So you understand what modules are and how import/export works. Great. But here's the thing CJS and ESM aren't just different syntaxes. Under the hood, they behave very differently. And those differences actually matter when you're building real apps.

Let's talk about the stuff most tutorials skip.

How They Actually Load Files

This is one of the biggest differences, and it affects performance.

CommonJS is synchronous. When Node.js hits a require(), it stops everything, reads the entire file, executes it, and then moves on. It's like stopping your car completely every time you need to check the map.

const data = require('./heavyFile.js'); // Everything pauses here
console.log("This runs AFTER the file loads");

This works fine on a server where files live on your hard drive and load in milliseconds. But it's a disaster for browsers, where files travel over a network and could take seconds.

ES Modules are asynchronous. The browser (or Node.js) can start fetching multiple modules at the same time without blocking everything else. Your app stays responsive while the files load in the background.

This is a big reason why ESM became the standard for the web.

Static vs Dynamic

This is where it gets really interesting, and most beginners never hear about it.

CommonJS is dynamic. The require() call can live anywhere in your code inside an if statement, inside a function, even based on user input:

// This is totally valid CJS
if (userRole === "admin") {
  const adminTools = require('./adminTools.js');
}

Sounds flexible, right? But here's the problem because the module path can be a variable, JavaScript has no idea what's being imported until the code actually runs. That means:

  • Bundlers can't tree-shake (remove unused code)

  • Security scanners can't see what's being loaded

  • A bad actor could potentially manipulate what gets required

Imagine this nightmare scenario:

const userInput = getInputFromSomewhere();
const module = require(userInput); // What is this loading?

If that userInput somehow comes from an untrusted source, you've got a serious security hole.

ES Modules are static. Every import statement must sit at the top of the file with a fixed path no variables, no conditions, no surprises:

// This is NOT allowed in ESM
if (condition) {
  import something from './file.js'; // Syntax error
}

That might feel restrictive, but it's actually a feature. Because the structure is fixed and predictable, tools can analyze your entire dependency tree before running anything. Security scanners know exactly what your app loads. Bundlers can eliminate dead code with confidence. Everything is transparent.

Live Bindings vs Copied Values

This one is subtle but genuinely catches developers off guard.

CJS gives you a copy of the exported value at the time of import. If that value changes later in the original file, your copy doesn't update.

// counter.cjs
let count = 0;
module.exports = { count, increment: () => count++ };

// main.cjs
const { count, increment } = require('./counter.cjs');
increment();
console.log(count); // Still 0 — you got a copy, not the real thing

ESM gives you a live binding. A live connection to the actual variable in the original file. If it changes, you see the change.

// counter.js
export let count = 0;
export const increment = () => count++;

// main.js
import { count, increment } from './counter.js';
increment();
console.log(count); // 1 — it updated!

This matters a lot when you're working with shared state, counters, flags, or anything that changes over time.

A Quick Side-by-Side to Tie It All Together

CommonJS (CJS) ES Modules (ESM)
Syntax require / module.exports import / export
Loading Synchronous (blocking) Asynchronous (non-blocking)
Analysis Dynamic (runtime) Static (before runtime)
Tree Shaking Limited Full support
Security Riskier with dynamic paths Safer — paths are fixed
Circular deps Silent broken values Handled more gracefully
Exported values Copied at import time Live bindings
Best for Legacy Node.js code Modern apps and browsers

"The secret to building large apps is never build large apps. Break your applications into small pieces. Then assemble those testable, bite-sized pieces into your big application."Justin Meyer

JavaScript

Part 14 of 24

Master JavaScript from the ground up with a structured, easy-to-follow learning path designed for serious developers. This series starts with core fundamentals like variables, data types, and control flow, then gradually moves into deeper topics such as functions, scope, prototypes, asynchronous programming, and how the JavaScript engine works internally. Each lesson focuses on clarity and real understanding. You will learn not only how to write JavaScript, but why it behaves the way it does. Concepts are explained using simple examples, practical use cases, and clean coding patterns that reflect real-world development.

Up next

The Magic of this, call(), apply(), and bind() in JavaScript

Understand this, call, apply and bind and their role in JS