The Complete Guide to Modules in Browsers and Node
Tags: javascript, programming
Published: 2022-10-10 23:08:15

by [Craig Buckler](https://blog.openreplay.com/authors/craig-buckler)


Most programming languages have a concept of modules: *a way to define functions in one file and use them in another*. Developers can create code libraries that are responsible for related tasks.

The benefits of modules include:

* you can split code into smaller files with self-contained functionality
* the same modules are reusable across any number of applications
* a module that's proven to work should not require further debugging
* it solves naming conflicts: function `f()` in `module1` should not be able to clash with function `f()` in `module2`.

Those migrating to JavaScript from another language would have been shocked to discover it had no concept of modules during the first two decades of its life. *There was no way to import one JavaScript file into another*.

Client-side developers had to:

* add multiple `<script>` tags to an HTML page

* concatenate scripts into a single file, perhaps using a bundler such as [webpack](https://webpack.github.io/), [esbuild](https://esbuild.github.io/), or [Rollup.js](https://rollupjs.org/), or

* use a dynamic module loading library such as [RequireJS](http://requirejs.org/) or [SystemJS](https://github.com/systemjs/systemjs), which implemented their own module syntax (such as AMD or CommonJS).


## Using ES2015 Modules (ESM)

ES Modules (ESM) arrived in ECMAScript 2015 (ES6). ESM offers the following features:

* Module code runs in strict mode -- there's no need for `'use strict'`.
* Everything inside an ES2015 module is private by default. The `export` statement exposes public properties, functions, and classes; the `import` statement can reference them in other files.
* You reference imported modules by URL -- *not a local file name*.
* All ES modules (and child sub-modules) resolve and import **before** the script executes.
* ESM works in modern browsers and server runtimes, including Node.js, Deno, and Bun.

The following code defines a `mathlib.js` module that exports three public functions at the end:

```javascript
// mathlib.js

// add values
function sum(...args) {
  log('sum', args);
  return args.reduce((num, tot) => tot + num);
}

// multiply values
function multiply(...args) {
  log('multiply', args);
  return args.reduce((num, tot) => tot * num);
}

// factorial: multiply all values from 1 to value
function factorial(arg) {
  log('factorial', arg);
  if (arg < 0) throw new RangeError('Invalid value');
  if (arg <= 1) return 1;
  return arg * factorial(arg - 1);
}

// private logging function
function log(...msg) {
  console.log(...msg);
}

export { sum, multiply, factorial };
```

You can also export public functions and values individually, e.g.


```javascript
// add values
export function sum(...args) {
  log('sum', args);
  return args.reduce((num, tot) => tot + num);
}
```

An `import` statement includes an ES module by referencing its **URL** path using relative notation (`./mathlib.js`, `../mathlib.js`) or fully-qualified notation (`file:///home/path/mathlib.js`, `https://mysite.com/mathlib.js`).

You can reference ES modules added using Node.js `npm install` using the `"name"` defined in `package.json`.

Modern browsers, [Deno](https://deno.land/), and [Bun](https://bun.sh/) can load modules from a web URL (`https://mysite.com/mathlib.js`). This is not natively supported in Node.js but will arrive in a future release.

You can `import` specific named items:

```javascript
import { sum, multiply } from './mathlib.js';

console.log( sum(1,2,3) );      // 6
console.log( multiply(1,2,3) ); // 6
```

Or you can alias an import to resolve any naming conflicts:

```javascript
import { sum as addAll, mult as multiplyAll } from './mathlib.js';

console.log( addAll(1,2,3) );      // 6
console.log( multiplyAll(1,2,3) ); // 6
```

Or `import` all public values using an object name as a namespace:

```javascript
import * as lib from './mathlib.js';

console.log( lib.sum(1,2,3) );      // 6
console.log( lib.multiply(1,2,3) ); // 6
console.log( lib.factorial(3) );    // 6
```

A module that exports a single item can be an anonymous `default`. For example:

```javascript
// defaultmodule.js
export default function() { ... };
```

Import the default *without curly braces* using any name you prefer:

```javascript
import myDefault from './defaultmodule.js';
```

This is effectively the same as the following:

```javascript
import { default as myDefault } from './defaultmodule.js';
```

Some developers avoid `default` exports because:

* Confusion can arise because you can assign any name, e.g., `divide`, for a default multiplication function. The module functionality could also change and make the name redundant.
* It can break code assistance tools such as refactoring in editors.
* Adding related functions into the library becomes more difficult. Why is one function the default when another is not?
* It's tempting to export a single default object literal with more than one property-addressed function rather than use individual `export` declarations. This makes it impossible for bundlers to tree-shake unused code.

I'd never say never, but there's little benefit in using default exports.


## Loading ES Modules in Browsers

Browsers load ES modules asynchronously, and execution defers until the DOM is ready. Modules run in the order specified by each `<script>` tag:

```html
<script type="module" src="./run-first.js"></script>
<script type="module" src="./run-second.js"></script>
```

And each inline `import`:

```html
<script type="module">
import { something } from './run-third.js';
// ...
</script>
```

Browsers *without* ESM support will not load and run a script with the `type="module"` attribute. Similarly, browsers *with* ESM support will not load scripts with a `nomodule` attribute:

Where necessary, you can provide two scripts for modern and old browsers:

```html
<script type="module" src="./runs-in-modern-browser.js"></script>
<script nomodule src="./runs-in-old-browser.js"></script>
```

This may be practical when:

* You have a large proportion of IE users.
* Progressive enhancement is difficult, and your app has essential functionality which you cannot implement in HTML and CSS alone.
* You have a build process that can output both ES5 and ES6 code from the same source files.

Note that ES modules must be served with the MIME type `application/javascript` or `text/javascript`. The [CORS header](https://developer.mozilla.org/Web/HTTP/CORS) must be set when a module can be imported from another domain, e.g., `Access-Control-Allow-Origin: *` to allow access from any site.

Be wary of importing third-party code from another domain. It will affect performance and is a security risk. When in doubt, copy the file to your local server and `import` from there.




<h2>Open Source Session Replay</h2> 

_<a href="https://github.com/openreplay/openreplay" target="_blank">OpenReplay</a> is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data._

![OpenReplay](https://blog.openreplay.com/banner-blog_1oYPGT.png)

_Start enjoying your debugging experience - <a href="https://github.com/openreplay/openreplay" >start using OpenReplay for free</a>._



## Using CommonJS Modules in Node.js

CommonJS was chosen as the module system for the server-side Node.js because ESM did not exist when the JavaScript runtime was released in 2009. You may have encountered CommonJS when using Node.js or `npm`. A CommonJS module makes a function or value publicly available using `module.exports`. Rewriting our `mathlib.js` ES module from above:

```javascript
// mathlib.js

// add values
function sum(...args) {
  log('sum', args);
  return args.reduce((num, tot) => tot + num);
}

// multiply values
function multiply(...args) {
  log('multiply', args);
  return args.reduce((num, tot) => tot * num);
}

// factorial: multiply all values from 1 to value
function factorial(arg) {
  log('factorial', arg);
  if (arg < 0) throw new RangeError('Invalid value');
  if (arg <= 1) return 1;
  return arg * factorial(arg - 1);
}

// private logging function
function log(...msg) {
  console.log(...msg);
}

module.exports = { sum, multiply, factorial };
```

A `require` statement includes a CommonJS module by referencing its **file** path using relative (`./mathlib.js`, `../mathlib.js`) or absolute notation (`/path/mathlib.js`). Reference modules are added using `npm install` using the `"name"` defined in `package.json`.

A CommonJS module is dynamically included and synchronously loaded at the point it's referenced during the execution of the script. You can `require` specific exported items:

```javascript
const { sum, mult } = require('./mathlib.js');

console.log( sum(1,2,3) );      // 6
console.log( multiply(1,2,3) ); // 6
```

Or you can `require` every exported item using a variable name as a namespace:

```javascript
const lib = require('./mathlib.js');

console.log( lib.sum(1,2,3) );      // 6
console.log( lib.multiply(1,2,3) ); // 6
console.log( lib.factorial(3) );    // 6
```

You can define a module with a single default exported item:

```javascript
// mynewclass.js
class MyNewClass {};
module.exports = MyNewClass;
```

`require` a default using any name:

```javascript
const
  ClassX = require('mynewclass.js'),
  myObj = new ClassX();
```

CommonJS can also import JSON data as a JavaScript object:

```javascript
const myData = require('./data.json');

console.log( myData?.myProperty );
```


## Differences Between ES Modules and CommonJS

ESM and CommonJS look superficially similar, but there are fundamental differences.

* CommonJS dynamically loads a file when encountering a `require` statement during execution.

* ESM hoists, pre-parses, and resolves all `import` statements **before** executing code.

Dynamic `import` of ES Modules is not directly supported or recommended - this code will fail:

```javascript
// WON'T WORK!
const script = `./lib-${ Math.round(Math.random() * 3) }.js`;
import * as lib from script;
```

It's possible to dynamically load ES modules using the asynchronous [`import()` function]((https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/import#dynamic_import)) which returns a promise:

```javascript
const script = `./lib-${ Math.round(Math.random() * 3) }.js`;
const lib = await import(script);
```

This affects performance, and code validation becomes more difficult. Only use the `import()` function when there's no other option, e.g., a script is dynamically created after an application has started.

ESM can also import JSON data, although this is not (yet) an approved standard, and support may vary across platforms:

```javascript
import data from './data.json' assert { type: 'json' };
```

The dynamic CommonJS vs. hoisted loading ESM can lead to other logic incompatibilities. Consider this ES module:

```javascript
// ESM two.js
console.log('running two');
export const hello = 'Hello from two';
```

This script imports it:

```javascript
// ESM one.js
console.log('running one');
import { hello } from './two.js';
console.log(hello);
```

`one.js` outputs the following when it's executed:

```
running two
running one
hello from two
```

This occurs because `two.js` imports **before** `one.js` executes even though the `import` comes after the `console.log()`.

A similar CommonJS module:

```javascript
// CommonJS two.js
console.log('running two');
module.exports = 'Hello from two';
```

Referenced in `one.js`:

```javascript
// CommonJS one.js
console.log('running one');
const hello = require('./two.js');
console.log(hello);
```

Results in the following output. The execution order is different:

```
running one
running two
hello from two
```

Browsers do not directly support CommonJS, so this is unlikely to affect client-side code. Node.js supports **both** module types, and it's possible to mix CommonJS and ESM in the same project!

Node.js adopts the following approach to resolve module compatibility problems:

* CommonJS is the default (or set `"type": "commonjs"` in `package.json`).
* Any file with a `.cjs` extension is parsed as CommonJS.
* Any file with a `.mjs` extension is parsed as ESM.
* Running `node --input-type=module index.js` parses the entry script as ESM.
* Setting `"type": "module"` in `package.json` parses the entry script as ESM.


One final benefit of ES modules is they support top-level `await`. You can execute asynchronous code in the entry code:

```javascript
await sleep(1);
```

This is not possible in CommonJS. It's necessary to declare an outer `async` Immediately Invoked Function Expression (IIFE):

```javascript
(async () => {
  await sleep(1);
})();
```


### Importing CommonJS Modules in ESM

Node.js can `import` a CommonJS module in an ESM file. For example:

```javascript
import lib from './lib.cjs';
```

This often works well, and Node.js makes syntax suggestions when problems occur.


### Requiring ES Modules in CommonJS

It's **not** possible to `require` an ES module in a CommonJS file. Where necessary, you can use the asynchronous `import()` function shown above:

```javascript
// CommonJS script
(async () => {

  const lib = await import('./lib.mjs');

  // ... use lib ...

})();
```


## Conclusion

ES Modules took many years to arrive, but we finally have a system that works in browsers and server-side JavaScript runtimes such as Node.js, Deno, and Bun.

That said, Node.js used CommonJS for half its life, and it's also supported in Bun. You may encounter libraries that are CommonJS only, ESM only, or provide separate builds for both. I recommend adopting ES Modules for new Node.js projects *unless* you encounter an essential (but rare) CommonJS package that is impossible to `import`. Even then, you could consider moving that functionality to a [worker thread](https://nodejs.org/api/worker_threads.html) or [child process](https://nodejs.org/api/child_process.html), so the rest of the project retains ESM.

Converting a large legacy Node.js project from CommonJS to ESM could be challenging, especially if you encounter the above execution order differences. Node.js will support CommonJS for years -- *perhaps forever* -- so it's probably not worth the effort. That may change if customers demand full ESM compatibility for your public libraries.

For everything else: *use ES Modules*. It's the JavaScript standard.

For more information, refer to the following:

* [JavaScript modules on MDN](https://developer.mozilla.org/docs/Web/JavaScript/Guide/Modules)
* [CommonJS modules on nodejs.org](https://nodejs.org/dist/latest/docs/api/modules.html)
* [ECMAScript modules on nodejs.org](https://nodejs.org/dist/latest/docs/api/esm.html)
* [Linking to Third Party Code in Deno](https://deno.land/manual/linking_to_external_code)

> A TIP FROM THE EDITOR: For a totally different kind of module, look at our [Using CSS Modules In React](https://blog.openreplay.com/using-css-modules-in-react/) article.




[![newsletter](https://blog.openreplay.com/newsletter_Z1P0UIP.png)](https://newsletter.openreplay.com/)