ES modules: import, export and live bindings
How import and export really work: named and default exports, live bindings, why modules are strict and deferred, and why each module runs only once.
Comes up in: JavaScript, Recruiter screen
Learn#
Anil splits a 3,000-line app.js into files. Then a bug: two files import the same counter, and he expects two separate counters. There is only one.
An ES module is a file with import and export. A file can have many named exports (export function increment) and one default export (export default). You import named exports by their exact name in braces; the default can take any name.
The imports form a module graph. Each module in it runs once, and every importer shares that one copy:
cat > counter.mjs <<'JS'
export let count = 0;
export function increment() { count++; }
console.log("counter.mjs runs");
JS
cat > main.mjs <<'JS'
import { count, increment } from "./counter.mjs";
import * as again from "./counter.mjs";
increment();
increment();
console.log(count, again.count, this, import.meta.url.endsWith("main.mjs"));
JS
node main.mjscounter.mjs runs
2 2 undefined truecount is a live binding: a read-only view of the exporter’s variable, not a copy. When counter.mjs changes it, importers see the new value.
State
- graph: main.mjs → counter.mjs
Console
empty
The engine reads main.mjs and finds its import statements before running any code.
Read the steps as text
- The engine reads main.mjs and finds its import statements before running any code.
- counter.mjs is fetched and run first. It prints once and creates the binding count = 0.
- main.mjs runs. Both imports point at the same module instance, so increment changes the one count.
- Both names read the live value 2. The module body did not run a second time.
Predict#
Predict first
In main.mjs, you write count = 5 after importing { count }. What happens?
Imported bindings are read-only views of the exporter’s variable. Only the exporting module can change them, for example through increment(). It is tempting to think a live binding works both ways.
Interview angle#
Say it like this — about 30 seconds
“ES modules use static import and export, so the module graph is known before code runs. Each module runs once and is shared. Imports are live, read-only bindings to the exporter’s variables. Modules are always strict, and this is undefined at the top level. In browsers, script type module is deferred by default. CommonJS uses require, which runs synchronously and returns the module.exports object, so values are not live.”
Follow-up What is a live binding?
Follow-up Why can bundlers tree-shake ES modules but not most CommonJS?
require can be called anywhere with any string (JS.10.04).Follow-up What is import.meta?
import.meta.url, its full URL or file path.Practice#
On your own
Build a small registry that follows the ES module rule: run each module once and share it.
Done when
loadruns a module’s factory only on the first load.- Every
loadof the same name returns the very same object. - Loading an unknown name throws an Error that names the module.
Remember#
Try to answer each card before you open it. Then add them to your reviews.
How many times does a module's top-level code run if five files import it?
Once. Every importer shares the same instance.
What is a live binding?
An imported name that always reflects the exporter's current value.
What is this at the top level of an ES module?
undefined
Can an importer assign to an imported binding?
No. It throws a TypeError; only the exporter can change it.
When does a script type=module run in the browser?
After the HTML is parsed, like a defer script.
Go deeper
Four layers. Stop at the one your interview needs; each ends with a quick check.
The core ideaEvery round
Named: export const x, imported as { x } or renamed { x as y }. Default: export default fn, imported as any name. Namespace: import * as m gives one object of all exports.
Check yourself: How do you import a default export and a named export together?
import chart, { formatDate } from './chart.js'
How it works underneathSenior rounds
Loading happens in three phases: construction finds and parses every file, linking connects imports to exports, and evaluation runs each module once, dependencies first Source: ECMA-262: Source Text Module Records. Because linking happens before any code runs, a misspelled import name is a SyntaxError at load time.
Check yourself: Name the three phases of loading an ES module graph.
Construction (find and parse files), linking (connect bindings), evaluation (run code).
Edge cases and trapsWhere people slip
this is undefined, and top-level variables stay inside the module rather than becoming globals.import.meta.url gives the module’s own URL, useful for loading files next to it.Check yourself: Why does a module script not see variables from another module's top level?
Each module has its own scope; top-level declarations are not globals.
In productionStaff-level answers
<script type="module"> is deferred by default and fetched with CORS rules, so it needs a web server, not file:// Source: MDN: JavaScript modules. <script nomodule> was the fallback for old browsers.Check yourself: Why does a module script fail when opened as file:// in the browser?
Module scripts are fetched with CORS rules, and file:// is not a valid origin for them.
Finished the five steps? Mark it, and its cards join your reviews.
Last checked 2026-10-31. Spot a mistake? Use “Report an error” below — fixes are listed on the corrections page.
Interview questions this answers
- What is the difference between CommonJS and ES modules?
- What is a live binding?