# 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] 

![](https://cdn.hashnode.com/uploads/covers/6950d1ca85602739c40abd67/6990f58b-7ca5-4926-a514-23a675334df2.png align="center")

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.

![](https://cdn.hashnode.com/uploads/covers/6950d1ca85602739c40abd67/0ca1c041-74da-45a9-8ee0-2646636b362f.png align="center")

## 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:**

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

module.exports = { sayHello };
```

**Bringing it in:**

```javascript
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**

```javascript
// math.js
export const add = (a, b) => a + b;
export const PI = 3.14;
```

```javascript
// 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**

```javascript
// User.js
export default class User {
  constructor(name) {
    this.name = name;
  }
}
```

```javascript
// 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:

```js
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.

![](https://cdn.hashnode.com/uploads/covers/6950d1ca85602739c40abd67/a511a07b-0242-4f7c-86ce-177beb69f47b.png align="center")

## 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.

```javascript
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:

```javascript
// 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:

```javascript
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:

```javascript
// 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.

![](https://cdn.hashnode.com/uploads/covers/6950d1ca85602739c40abd67/fe39117f-3c48-433d-ae6d-2238a3412132.png align="center")

## 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.

```typescript
// 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.

```javascript
// 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.

![](https://cdn.hashnode.com/uploads/covers/6950d1ca85602739c40abd67/3c6ac652-7596-4916-8ebb-9b7f6365cbc0.png align="center")

## 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 |

![](https://cdn.hashnode.com/uploads/covers/6950d1ca85602739c40abd67/af655c22-ac46-4b31-9252-762ac5f61d25.png align="center")

> *"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**
