JavaScriptModules Overview

Modules Overview

A module is a JavaScript file that has its own private scope and explicitly chooses what to share with the outside world. Anything declared inside a module — variables, functions, classes — is hidden by default. To make something usable from another file you export it; to consume something from another file you import it. Modules are how real JavaScript codebases stay organised once they grow past a single page of script.

Before modules: one big global

In old-school browser code, every <script> shared the same global object (window in the browser). Variables declared at the top level of one file were visible everywhere, which made name collisions almost guaranteed and load order load-bearing.

Pre-module pain

HTML
<script src="utils.js"></script>      <!-- defines window.format -->
<script src="vendor.js"></script>     <!-- also defines window.format (oops) -->
<script src="app.js"></script>        <!-- which one wins? whoever loaded last -->

Workarounds — IIFEs, the "namespace object" pattern, AMD, CommonJS — kept the lights on for years. ES2015 finally added modules to the language itself.

What a module gives you
  • File-level scope. Top-level let, const, function, and class declarations are private to the file. They never leak onto window or the global object.

  • Explicit boundaries. A glance at the import and export lines tells you exactly what a file consumes and what it offers — no hidden globals.

  • Strict mode by default. Every module runs in strict mode, even without the "use strict" directive.

  • Static structure. Imports and exports are resolved before code runs, which lets bundlers tree-shake unused exports and lets tools statically check what each file uses.

  • Top-level await. A module is allowed to await directly at the top level (more on this in Top-Level await).

A minimal example

math.js

JS
// Anything you want callers to use, you export.
export function add(a, b) {
  return a + b;
}

export const PI = 3.14159;

// Anything else stays private to this file.
function internalHelper() {
  return "you can't see me from outside";
}

app.js

JS
import { add, PI } from "./math.js";

console.log(add(2, 3));     // 5
console.log(PI);            // 3.14159
// console.log(internalHelper); // ReferenceError — not exported
5
3.14159
Loading modules in the browser

Browsers only treat a file as a module when you ask them to. Mark the entry script with type="module":

index.html

HTML
<!DOCTYPE html>
<html>
  <body>
    <script type="module" src="./app.js"></script>
  </body>
</html>

A few rules follow from that:

  • Module scripts are deferred by default — they run after the HTML is parsed, in document order.

  • Module specifiers must be a URL or a relative path (e.g. "./math.js"). Bare names like "lodash" need a bundler or an import map.

  • Modules are fetched with CORS, so you usually need to serve them from a real HTTP server, not file://.

  • Each module is fetched and executed exactly once, even if many files import it.

Why modules are worth the small ceremony
  • Organisation. You can split a 5000-line script into small files with names that say what they do.

  • Encapsulation. Helper functions can stay helpers — no risk of becoming an accidental public API.

  • Reuse. The same module can be imported from many places without re-declaring globals.

  • Lazy loading. With import() you can defer loading a chunk until the user actually needs it. See Dynamic Imports.

  • Tooling. Static analysis, dead-code elimination, type checking and refactors all rely on knowing what each file imports.

Modules vs classic scripts at a glance
  • Top-level scope — module: private to the file; classic script: shared global.

  • Strict mode — module: always; classic: only with "use strict".

  • this at the top level — module: undefined; classic: the global object.

  • Load timing — module: deferred; classic: synchronous unless you add defer/async.

  • import / export — module: works; classic: syntax error.

Modules are evaluated once
If `math.js` is imported by both `app.js` and `utils.js`, the engine fetches and runs it a single time, and both importers see the same exported bindings. This makes modules a natural place to keep singletons — but it also means side effects at the top of a module run exactly once, not every time someone imports it.
Use the .js extension in the browser
Browsers do not invent extensions for you — `import x from "./math"` will 404 if the file is `math.js`. Bundlers like Vite and webpack hide this with resolvers, but native ES modules in the browser require the exact path.

The next pages dig into the syntax — import / export, Dynamic Imports, and how ES modules compare to Node's older CommonJS format.