Live Bindings and Module Cycles
An ES module import is a continuously updated reference, not a copied value — the mechanism that makes many circular imports resolvable.
Purpose
An ES module import binds to the exporting module’s actual variable, live — reading the import
always reflects the exporting module’s current value, not a snapshot taken at import time.
Example
// counter.js
export let count = 0;
export function increment() {
count++;
}
// main.js
import { count, increment } from './counter.js';
console.log(count); // 0
increment();
console.log(count); // 1 — the same binding, updated
Behavior notes
- CommonJS
require, by contrast, copies the exported value(s) at require time — mutating the original module’s internal state afterward is not reflected in an already-required copy for a primitive value (an object reference is still shared, but reassigning the exported binding itself is not visible). - A module cycle (A imports B, which imports A) can resolve correctly specifically because bindings are live — as long as nothing tries to use a binding’s value before the defining module finishes evaluating it.
- Using a live binding at a cyclic module’s top level, before the cycle resolves, throws a “cannot access before initialization” error — a real constraint, not just a style preference.
Common mistakes
- Assuming an ES module import behaves like a CommonJS
require— copied once, not updated. - Structuring a circular dependency so a top-level statement in one module immediately uses a binding from the other, rather than deferring that use inside a function called later.
Runtime notes
Core ECMAScript module semantics, identical across every current browser and Node version.