A module is a file parsed with the Module goal (ES2015; ECMA-262, Modules).
It has its own scope, is always strict, and
shares names only through import and export. How a file comes
to be a module is on File
structure.
Import declarations
ImportDeclaration:
import ImportClause FromClause [WithClause] ;
import ModuleSpecifier [WithClause] ;
ImportClause:
ImportedDefaultBinding
NameSpaceImport
NamedImports
ImportedDefaultBinding , NameSpaceImport
ImportedDefaultBinding , NamedImports
NameSpaceImport:
* as ImportedBinding
NamedImports:
{ }
{ ImportsList [,] }
ImportSpecifier:
ImportedBinding
ModuleExportName as ImportedBinding
FromClause:
from ModuleSpecifier
ModuleSpecifier:
StringLiteral
| Form | Binds |
|---|---|
import x from 'm' | The default export, as x |
import { a, b as c } from 'm' | Named exports, the second one renamed |
import { 'a-b' as c } from 'm' | A string-named export |
import * as ns from 'm' | The module’s namespace object |
import x, { a } from 'm' | Default and named |
import x, * as ns from 'm' | Default and namespace |
import 'm' | Nothing: runs the module for its side effects |
Export declarations
ExportDeclaration:
export ExportFromClause FromClause [WithClause] ;
export NamedExports ;
export VariableStatement
export Declaration
export default HoistableDeclaration
export default ClassDeclaration
export default [lookahead ∉ { function, async function, class }] AssignmentExpression ;
ExportFromClause:
*
* as ModuleExportName
NamedExports
ExportSpecifier:
ModuleExportName
ModuleExportName as ModuleExportName
ModuleExportName:
IdentifierName
StringLiteral
| Form | Exports |
|---|---|
export const a = 1; | A declaration, under its own name. Also let, var, function, class |
export { a, b as c }; | Existing bindings, one renamed |
export { a as 'a-b' }; | Under a string name (ES2022) |
export default expr; | The default export, from a value |
export default function f() {} | The default export, as a hoisted function |
export { a } from 'm'; | Re-exports, without a local binding |
export * from 'm'; | Re-exports all but default |
export * as ns from 'm'; | The namespace, as one name (ES2020) |
- An exported name must be unique in the module, or it is a syntax error.
export defaultof an expression has no local name: internally it is the binding*default*.- If two
export *clauses provide the same name, the name is ambiguous. It is not exported, and importing it is an error.
Linking and evaluation
A host loads a module graph in phases before any module runs:
- Load and parse. Each module is fetched, then parsed, and its imports found.
- Link. Imports are connected to the exporting bindings, and
missing names are errors (a
SyntaxError). - Evaluate. Modules run once each, children first, in depth-first post-order.
Imports are live bindings: the importer sees the exporter’s variable, and
changes show through. Importers cannot assign to them, so
x = 1 on an imported x throws a TypeError.
// counter.js
export let count = 0;
export function up() { count++; }
// main.js
import { count, up } from './counter.js';
up();
console.log(count); // 1
Cycles
Cycles are allowed because linking finishes first. If a module reads a binding
from a module that has not yet run, the read is in its
dead zone
and throws a ReferenceError. Hoisted functions are ready, so they are
safe to call across a cycle.
Module specifiers
What a specifier means is left to the host.
| Specifier | Browser | Node.js |
|---|---|---|
./a.js, ../b.mjs | Relative URL | Relative file; the extension is required for ES modules |
/lib/a.js, https://… | URL | A file path or a file: or data: URL |
lodash | Only through an import map | A package from node_modules, or a built-in |
node:fs | Unsupported | A built-in module |
#dep | Unsupported | The "imports" field of package.json |
Namespace objects
import * as ns gives a Module Namespace Exotic Object. Its keys are the
export names, sorted by code unit. It has no prototype, its
Symbol.toStringTag is "Module", it cannot be extended, and
all its properties are read-only live views of the bindings.
import * as maths from './maths.js';
Object.keys(maths); // sorted export names
maths.add(1, 2);
maths.default; // the default export, if any
Dynamic import
ImportCall:
import ( AssignmentExpression [, AssignmentExpression] [,] )
import(specifier, options) (ES2020) is an expression that returns a promise
for a namespace object. It works in scripts and in modules. The specifier may be
computed, and options may hold { with: { type: 'json' } }.
const { default: plugin } = await import(`./plugins/${name}.js`);
Import attributes
WithClause:
with { }
with { WithEntries [,] }
WithEntries:
AttributeKey : StringLiteral
AttributeKey : StringLiteral , WithEntries
import data from './data.json' with { type: 'json' };
const css = await import('./style.css', { with: { type: 'css' } });
type attribute says how to load a non-JavaScript module- Import attributes and JSON modules are in ES2025. A JSON module has one
export,
default. - A host rejects attribute keys it does not support.
- An earlier draft used the keyword
assert, which has been replaced bywith. - In browsers,
type: 'css'loads a CSS module script. In Node.js,type: 'json'is required when importing JSON.
import.meta
import.meta (ES2020) is an object created by the host for each module. It
is a syntax error in a script. ECMA-262 defines no properties of its own.
| Property | Host | Value |
|---|---|---|
import.meta.url | Browsers, Node.js | The module’s URL |
import.meta.resolve(s) | Browsers, Node.js | The URL that a specifier would resolve to |
import.meta.filename | Node.js 21.2, 20.11 | The absolute path of the file |
import.meta.dirname | Node.js 21.2, 20.11 | Its folder |
Top-level await
// config.js
const res = await fetch('/config.json');
export const config = await res.json();
await at the top level of a module (ES2022) makes the module async. Its
importers wait for it, while unrelated sibling modules keep running. A rejected
module makes everything depending on it fail. Evaluation stays in order: sibling
modules start in source order, and any importer waits until its dependencies are done.
It is not allowed in a script, where await is an identifier.
CommonJS and interop
CommonJS is the module system Node.js used before ES modules, and is not part of ECMA-262. Node.js wraps each file in a function, and runs it as a script:
(function(exports, require, module, __filename, __dirname) {
// the module’s code is here
});
CommonJS (.cjs) | ES module (.mjs) | |
|---|---|---|
| Load | require('x'), synchronous | import, asynchronous |
| Export | module.exports = v, exports.a = v | export declarations |
| Bindings | A copy of a property value when read | Live bindings |
| Evaluated | When require runs | Before the importer, in graph order |
Top-level this | module.exports | undefined |
Top-level await | No | Yes |
| File path | __filename, __dirname | import.meta.filename, import.meta.dirname |
| Strict by default | No | Yes |
| Cycles | A partly filled exports object | Live bindings with dead zones |
exportsstarts out as the same object asmodule.exports. Assigning to it just rebinds the local variable, so onlymodule.exports = …replaces what is exported.- Importing CommonJS from an ES module: the default export is
module.exports. Node.js also finds named exports by static analysis of the source. That detection misses exports added dynamically, and those are not live bindings. - Requiring an ES module from CommonJS:
require()can load a synchronous ES module, returning its namespace object. It is enabled without a flag in Node.js 22.12, 20.19 and 23.0 and later. If the module or its graph uses top-levelawait, it throwsERR_REQUIRE_ASYNC_MODULE; useimport()for those. - An ES module can choose the exact value for
require()by exporting it under the string name'module.exports'. module.createRequire()builds arequirefunction for use in an ES module.