Skip to main content

Menu

System onlineAI Tutorials / 2026

AI Tutorials
Knowledge search

Recent · Popular · Tutorials

  • dart3 tutorials
  • java1 tutorial
  • Javascript3 tutorials
  • Node.js Introduction2 tutorials
  • dartdart · General · In depth
  • JavascriptJavascript · Advanced · In depth
  • JavascriptJavascript · Advanced · Mid
  • JavascriptJavascript · Advanced · Short

Find a tutorial

Tell the assistant what you want to learn. It suggests published chapters.

For example: “I want to learn Rust ownership from the start.” The conversation is kept only in this tab.

0 / 2,000 characters

Enter to send, Shift+Enter for a new line. Do not include personal information.

Chapter 13

Modules

Prerequisites assumed: closures, lexical scope, `import`/`export` syntax, and the browser/Node execution model. This chapter covers how ES modules actually load, evaluate, and link — and where they differ from CommonJS.
Level: AdvancedLength: Mid2 min read

Read at another depth

Modules

Prerequisites assumed: closures, lexical scope, import/export syntax, and the browser/Node execution model. This chapter covers how ES modules actually load, evaluate, and link — and where they differ from CommonJS.

Static Structure

ES modules are static: their imports and exports are resolved before any code runs. The parser walks the file, collects import declarations, and builds a dependency graph before evaluation. This is why import must be at the top level — you cannot import conditionally or from inside a function with the static syntax.

Code example in javascript
// logger.js
export function log(msg) {
  console.log(`[${new Date().toISOString()}] ${msg}`);
}
export default class Logger { /* ... */ }

// app.js — bindings, not copies
import Logger, { log } from './logger.js';
log('started');

The key distinction from CommonJS: import { log } creates a live binding to the exporting module's variable. If logger.js later reassigns an exported let, importers see the new value. In CommonJS, require() copies values at require time — objects are shared, primitives are not.

Evaluation Semantics

  1. Parse — the loader resolves all specifiers and fetches the graph.
  2. Instantiate — module records are built; exported bindings are linked before execution.
  3. Evaluate — module bodies run once, depth-first, in dependency order. The result is cached.

Because evaluation is cached per-URL, a module is a natural singleton. Cycles are allowed but risky: during evaluation of a cyclic import, bindings exist but are uninitialized, so accessing a const binding too early throws a ReferenceError (a temporal dead zone violation across module boundaries).

Code example in javascript
// a.js
import { b } from './b.js';
export const a = 'a';
console.log('a evaluates', b);

// b.js
import { a } from './a.js';
console.log('b evaluates', typeof a); // 'undefined' — TDZ if read before init
export const b = 'b';

Dynamic Import

import() returns a promise and can appear anywhere, enabling code splitting:

Code example in javascript
button.addEventListener('click', async () => {
  const { renderChart } = await import('./chart.js');
  renderChart(data);
});

This trades static analyzability for laziness. Bundlers can only split code they can see statically; fully dynamic specifiers (await import(someVariable)) defeat tree-shaking.

Interop and Gotchas

ConcernES ModulesCommonJS
Load timeStatic, parse-timeDynamic, run-time
Exported valuesLive bindingsCopied at require time
`this` at top level`undefined``module.exports`
Cycle behaviorTDZ risk, deterministicPartially-filled `exports` object
CachingPer URL, oncePer resolved path, once

In Node, a file is an ES module only if "type": "module" is set in package.json or the extension is .mjs. The most common failure is mixing systems: require() cannot load an ES module synchronously, and top-level await only works in modules.

A flowchart showing parse, instantiate, and evaluate phases, with evaluation traversing dependencies depth-first before the importing module's body runs.
100%
Module loading and evaluation order
Show diagram source
flowchart TD
  A[Parse entry module] --> B[Resolve all imports recursively]
  B --> C[Instantiate: link bindings]
  C --> D[Evaluate dependencies depth-first]
  D --> E[Evaluate module body once]
  E --> F[Cache result per URL]

Exercise

Why does mutating an imported binding throw a SyntaxError, while mutating a property of an imported object succeeds?

Show answer

Import bindings are read-only references to the exporting module's variable — reassigning them is a static error. If the binding holds an object, the exporting module owns the reference, and nothing prevents any module from mutating that object's internals.

Exercise

In a cyclic import a.js → b.js → a.js, when does b.js see a as initialized?

Show answer

Only after a.js's body finishes executing the const a = 'a' declaration. If b.js reads a during a.js's evaluation but before that line runs, it hits the temporal dead zone and throws. Reorder initialization or move the shared value into a third module.

Spotted a mistake, or code that does not work?

Report an issue

Chapter: Modules

0 / 2,000 characters

Plain text, for example: the second code sample does not compile. Do not include personal information.

You can't do that