Glossary / ES modules
ES modules
ECMAScript modules (ES modules, or ESM) are JavaScript's standardized module system. A module has its own top-level scope, declares imports and exports, and is linked and evaluated by a host such as a browser or Node.js. ESM enables static dependency analysis, cyclic linking, live bindings, and asynchronous module loading features such as top-level await.
Module structure and bindings
A file is treated as a module according to its host and configuration. In browsers, use <script type="module">; in Node.js, .mjs or a package's "type": "module" makes the intent explicit. Module top-level declarations do not become global properties. Each module is evaluated once per resolved module identity in a given loader/cache context.
// math.js
export const pi = 3.14159;
export function area(radius) { return pi * radius ** 2; }
// app.js
import { area } from './math.js';
console.log(area(2));
Imports are read-only live bindings: if the exporting module updates an exported let, importers observe the current value but cannot assign to the imported binding. A default export is one export under the special default name; named exports make API changes and refactoring more explicit. Re-export syntax can form a package facade.
Linking, evaluation, and cycles
Static imports are parsed before execution. The loader resolves the graph, creates module environments, links imports/exports, then evaluates modules according to dependency relationships. Cycles are supported, but accessing a lexical export before its initialization can throw a temporal-dead-zone ReferenceError; side effects during cyclic initialization are order-sensitive. Top-level await makes a module graph asynchronous and affects importers.
Browser resolution follows URL and server rules; Node resolution follows Node's ESM rules; bundlers may implement their own resolution and transforms. Do not assume extension omission, directory indexes, aliases, or package conditions work identically across these hosts.
Engineering guidance
Use ESM as the default for new JavaScript when your deployment and dependencies support it. Keep module initialization small and predictable, avoid hidden import-time side effects, and define package entry points with explicit exports. Test the actual runtime and bundler configuration, especially for dual-format packages and cycles.
Interview questions
- What is a live binding? An imported name reflects the exporting module's binding as it changes; the importer cannot assign to it.
- Are module declarations global? No; each module has its own top-level scope.
- How are cycles handled? The loader links the graph, but evaluation order can expose uninitialized lexical bindings.
- Why are imports static? Static declarations let hosts analyze and link dependencies before executing the module body.
- Is browser resolution identical to Node? No; each host defines resolution behavior, and bundlers add their own rules.