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 15

Modules

This chapter assumes you are comfortable with functions, closures, scope, and the event loop. We cover ES modules (ESM): their syntax, their static structure, how they differ from CommonJS (CJS), how cycles resolve, and the failure modes you will hit in real codebases.
Level: AdvancedLength: In depth3 min read

Read at another depth

Modules

This chapter assumes you are comfortable with functions, closures, scope, and the event loop. We cover ES modules (ESM): their syntax, their static structure, how they differ from CommonJS (CJS), how cycles resolve, and the failure modes you will hit in real codebases.

Why modules exist

Before ES2015, JavaScript had no built-in module system. Code was shared through globals, IIFEs, and loaders like AMD or the eventual de facto standard, CommonJS in Node. ESM was added to the language itself, which changed what tools can do with your code: because imports are resolved statically, before the program runs, bundlers, minifiers, and type checkers can see the whole dependency graph without executing it.

Syntax and semantics

Code example in javascript
// math.js — a module's exports
export const PI = 3.14159;
export function area(r) { return PI * r * r; }
export default function describe() { return 'circle math'; }
Code example in javascript
// app.js — importing
import describe, { PI, area as circleArea } from './math.js';
console.log(circleArea(2), PI, describe());

Key properties:

  • Each file is a module with its own top-level scope. Top-level const in a module does not become a global.
  • Modules are parsed, dependencies resolved, then evaluated once per graph. Repeated imports of the same module share a single instance.
  • Module code runs in strict mode automatically.
  • Declarations are hoisted, but evaluation order follows the dependency graph: dependencies evaluate before their importers (depth-first, post-order).

Live bindings

This is the single most misunderstood ESM behavior. import binds a name to the exporting module's variable, not to a snapshot of its value. If the exporter reassigns that variable, importers see the change:

Code example in javascript
// counter.js
export let count = 0;
export function inc() { count += 1; }
Code example in javascript
// main.js
import { count, inc } from './counter.js';
console.log(count); // 0
inc();
console.log(count); // 1 — the binding is live

You cannot reassign an imported binding from the importing side (count = 5 throws in strict mode); only the exporting module can mutate it. Default exports of objects are the common workaround for mutable state, but note that export default is just a named binding called default with a live binding all the same.

ESM vs CommonJS

These are not interchangeable, and the differences cause real bugs:

AspectESMCommonJS
LoadingStatic; analyzed before executionDynamic; `require` runs at call time
BindingsLiveCopied at require time (`module.exports`)
CyclesSupported; hoisted bindings may be uninitializedReturns a partial `exports` object
Top-level `this``undefined``module.exports`
File extension`.mjs` or `"type": "module"``.js` by default

Cycles

Circular imports are legal but dangerous. During a cycle, some module bodies have not finished evaluating, so their bindings may still be uninitialized (in the temporal dead zone):

Code example in javascript
// a.js
import { b } from './b.js';
export const a = 'a';
export function callB() { return b; } // fine: runs later
// console.log(b) here would throw ReferenceError
Code example in javascript
// b.js
import { a, callB } from './a.js';
export const b = 'b';
console.log(callB()); // 'b'

Failure mode: reading an imported binding at module top level in a cycle risks a ReferenceError: Cannot access 'X' before initialization. Functions are safe because they are only called later, after both modules evaluated. Decision rule: if a cycle exists, make sure cross-module reads happen inside functions, or better, break the cycle by extracting the shared code into a third module.

Dynamic import and import.meta

import() is not static; it returns a promise and can be called conditionally, enabling code splitting:

Code example in javascript
async function loadChart() {
  const { render } = await import('./chart.js'); // fetched on demand
  render();
}
if (needsChart) loadChart();

import.meta carries module metadata. import.meta.url is the module's absolute URL (or file URL in Node); it is the standard way to resolve asset paths relative to a module instead of the working directory:

Code example in javascript
const url = new URL('./data.json', import.meta.url);
const res = await fetch(url);
console.log(await res.json());

Bundlers, tree shaking, and gotchas

Because static imports cannot be computed at runtime, bundlers can tree shake: drop unused exports. This only works if your modules are side-effect-free. Two gotchas:

  1. Top-level side effects defeat shaking. import './polyfill.js' must be kept, which is why packages declare "sideEffects": false in package.json — an explicit contract, not something the tool can infer.
  2. Re-export patterns matter. Prefer export { x } from './x.js' over importing into an intermediate object; barrel files (index.js re-exporting everything) can force bundlers to include whole subtrees.

Exercise

Exercise

In counter.js (ESM) you export let count = 0, and in main.js you write count = 10. What happens, and why does the same pattern in CommonJS behave differently?

Show answer

main.js throws a TypeError (assignment to a constant-like import binding in strict mode). Only the exporting module may reassign count. In CommonJS, require('./counter.js') returns a snapshot of module.exports at require time, so the importer's copy would be an independent value that it could freely mutate — silently diverging from the original.

Exercise

a.js imports config from b.js; b.js imports setup from a.js. a.js reads config.name at its top level and crashes with "Cannot access 'config' before initialization". Give two fixes and the trade-off.

Show answer

(1) Move the read into a function called after both modules evaluate — trade-off: the value is available only at call time, which may hide the timing dependency. (2) Extract the shared config into its own module imported by both, eliminating the cycle — trade-off: one more file, but the dependency graph becomes acyclic and easier to reason about.

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