---
title: 'The Need for Modules'
source: 'https://academia.sh/en/courses/javascript-ecosystem/need-for-modules'
course: 'Modules, Tooling and the Ecosystem'
language: en
updated: '2026-08-17T18:09:45+00:00'
license: 'CC BY-SA 4.0'
---

# The Need for Modules

The name-collision problem born from sharing the global scope, the namespace pattern built with closures, and module scope's answer to that problem.

The Asynchronous JavaScript and the Runtime course covered how a single program runs
and how its problems are diagnosed. This course moves up one level: to the question of
how the pieces find one another, and who can see what, once a program no longer fits in
one file.

The question looks innocent. Splitting code into two files is assumed to be nothing
more than loading the files one after another. Seeing where this assumption breaks also
explains why the concept of a module was added to the language.

## The Global Scope Is a Shared Resource

The language's first unit of execution was the **script**: source text is parsed, and
its top-level declarations are written to the global scope. Two separate files, loaded
one after another in the same runtime, do not each build a separate world; both write to
the **same** global object.

This behavior can be observed directly through the runtime's script-evaluation
interface:

```javascript
// file: scripts.mjs
import vm from 'node:vm';

// Two separate "scripts", run in the same global scope.
vm.runInThisContext('var version = "chart-1.0"; function format(d) { return "chart: " + d; }');
vm.runInThisContext('var version = "report-2.0"; function format(d) { return "report: " + d; }');

console.log(globalThis.version);
console.log(globalThis.format('x'));
```

```
$ node scripts.mjs
report-2.0
report: x
```

The first script's `version` variable and `format` function vanished. No error, no
warning: the second declaration wrote over the first. The program keeps running and
produces the **wrong** result.

The problem's size grows not with the number of files but with the **number of
names**. If twenty files each define five global names, a shared pool of a hundred names
forms, and keeping that pool collision-free depends on nothing but the authors' care.
The scope concept established in the Programming Fundamentals course turns here into an
operational problem: a shared, writable namespace is an **unisolated resource**.

The second problem is **order**. If one file uses a function another defines, load
order matters — but that order is not written in the code itself; whoever loads it has
to know it separately. Dependency information sits **outside** the source, not inside
it.

## The Namespace Built with Closures

Before the language had module syntax, the answer to these two problems was to use
closures' scope-isolation property. A function is defined and called immediately, its
inner names stay inside that function's scope, and only a chosen object is handed
outward:

```javascript
// file: iife.mjs
var Measurer = (function () {
  var separator = /\s+/;
  function words(text) { return text.trim().split(separator); }
  return { words: words };
})();

console.log(Measurer.words('one two  three'));
console.log(typeof separator);
```

```
$ node iife.mjs
[ 'one', 'two', 'three' ]
undefined
```

`separator` is not visible from outside; only the name `Measurer` is written to the
global scope. This pattern is a direct application of the immediately invoked function
pattern from the Objects and Functions in JavaScript course, and it delivers a real
gain: **one** global name instead of a hundred.

It also loses as much as it gains. The namespace object itself is still in the global
scope, and it can still collide. Dependencies are still carried by convention: if
`Measurer` expects some other namespace to already be loaded, it has no written way to
declare that. Beyond that, finding where a given name comes from means scanning every
file, not reading the source.

## Module Scope

A **module** solves these problems at the language's parsing level. When a source text
is parsed as a **module**, its top-level declarations are written not to the global
scope but to a scope specific to that module:

```javascript
// file: scope.mjs
const version = 'text-measurer-1.0';
function format(d) { return 'measurer: ' + d; }

console.log(typeof globalThis.version);   // the module-scope name is not on the global object
console.log(typeof globalThis.format);
console.log(this);                        // this at the top level of a module
```

```
$ node scope.mjs
undefined
undefined
undefined
```

All three lines of output say the same thing: a module's top level is **not** the
global scope. The names `version` and `format` are not visible from outside; the
top-level `this` is bound not to the global object but to `undefined`.

The difference from the namespace pattern is **where the isolation comes from**. In
the closure pattern, isolation was a structure the programmer built, and it could
break; in a module, isolation is the parser's decision, and it cannot break.

## The Script/Module Distinction

The same source text can be parsed under two different rule sets. The distinction is
not limited to scope alone:

| Criterion | Script | Module |
|---|---|---|
| Top-level declarations | Written to the global scope | Stay in module scope |
| Top-level `this` | The global object | `undefined` |
| Strict mode | Must be requested explicitly | Always on |
| Import/export syntax | Invalid | Valid |
| Evaluation timing | The moment it is encountered | After dependencies are resolved |

The last row is decisive and will be detailed in the next lesson. Which rule a text
gets parsed under is decided by a marker the runtime reads: the file extension, the
`type` field in the manifest, or type information given by whoever loads the script.
The same character sequence takes on a different meaning depending on that marker.

## What Is Expected of a Module System

The problems a module system has to solve can be drawn straight from the two
experiments above:

**Isolation.** A module's internal names should not be visible from outside; what it
hands outward should be the module's own choice.

**Explicit interface.** What a module depends on and what it offers should be written
in the source itself. Dependency information should live in the code, not in the
loader's memory.

**Deterministic resolution.** A dependency name should map to the same file in every
environment. The name-resolution rule should be written down and predictable.

**Single evaluation.** When the same module is requested from more than one place, it
should be evaluated once; the state it holds should be the same for every consumer.
This guarantee is the language-level counterpart of the symbol resolution introduced in
the linking-and-loading lesson of the How Computers Work course: a name binds to exactly
one definition.

These four guarantees are the criterion for every mechanism examined throughout this
course. Package resolution is an implementation of the third; caching is an
implementation of the fourth.

## The Course's Example

One concrete example will be built throughout the course: a small library package
called `text-measurer`. Its job is computing a given text's word and sentence count,
average word length, and longest word. It starts as a single-file script; it gets split
into modules, its export surface gets defined, it gains a dependency, its version gets
tagged, it becomes part of a workspace, and it finally gets wired into build and
analysis tools.

Its first form is a single file, and it exports nothing:

```javascript
// file: meter.mjs
const WORD_SEPARATOR = /[^\p{L}\p{N}]+/u;
const SENTENCE_SEPARATOR = /[.?]+/;

function splitWords(text) {
  return text.split(WORD_SEPARATOR).filter((s) => s.length > 0);
}

function splitSentences(text) {
  return text.split(SENTENCE_SEPARATOR).map((c) => c.trim()).filter((c) => c.length > 0);
}

function averageLength(words) {
  if (words.length === 0) return 0;
  return words.reduce((t, s) => t + s.length, 0) / words.length;
}

function measure(text) {
  const words = splitWords(text);
  return {
    wordCount: words.length,
    sentenceCount: splitSentences(text).length,
    averageLength: averageLength(words),
  };
}

console.log(measure('A module carries its own scope. A script runs in the global scope.'));
```

```
$ node meter.mjs
{ wordCount: 13, sentenceCount: 2, averageLength: 4 }
```

Three separate responsibilities are interleaved in this file: splitting the text,
computing numeric measures, and presenting the result. Since all three share the same
scope, they see each other's names directly. As the file grows, this visibility stops
being a design decision and turns into a source of accidents: the `WORD_SEPARATOR`
constant is exposed to the entire file even though only the splitting functions care
about it.

The next lesson splits this file along its responsibilities and examines the effect
of that split on the export surface. Splitting's real question is not which code goes
into which file, but **which name stays visible from outside.**

## Summary

- A source parsed as a script writes its top-level declarations to the global scope;
  when two files define the same name, the second silently overrides the first.
- The namespace pattern built with closures reduces the number of global names, but
  leaves isolation to the programmer's discipline and does not declare dependencies in
  the code.
- A source parsed as a module keeps its top-level declarations in module scope; strict
  mode is always on, and top-level `this` is `undefined`.
- A module system provides four guarantees: isolation, an explicit interface,
  deterministic resolution, and single evaluation.

## Next Step

Module scope keeps names from leaking out — but we have not yet defined what a module
deliberately hands to another module. The next lesson takes up the language's standard
module syntax: the forms of export, the binding behavior of import, and why this
syntax's static structure is not just a matter of writing style.
