skip to content

ES Module Semantics

The rules ESM adds on top of plain scripts: a static import/export structure the engine analyzes before running any code, bindings that stay live, and a private strict-by-default module scope. Interviewers start here because every later question about interop, cycles, and tree shaking rests on these rules.

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

explore

questions

17

In ES modules, what is the difference between a named export and a default export, and how does the import syntax differ for each?

level: juniorimportance: must knowfreq 80%

answer

  1. one export table, two spellings
  2. default is just a name
  3. importer picks the default's name
  4. braces must match the exported name
  5. at most one default per module

basics

~20 s

A named export binds a value to a specific export name that importers must match exactly, using braces: import { parse } from './m.js'. A default export registers the value under the name "default", and the importer may bind it to any identifier it likes.

solid answer

~50 s

Both are entries in the same export table; the only difference is the name. `export const parse = ...` or `export { parse }` registers the export name `parse`, and an importer must spell it identically inside braces — `import { parse } from './m.js'` — optionally renaming with `import { parse as p }`. `export default value` registers the export under the reserved name `default`, and the importer writes it without braces and chooses the local name freely: `import anythingAtAll from './m.js'`. Because it is just a name, `import { default as parse }` is exactly equivalent to the default import, and a namespace import exposes it as `ns.default`. A module may have many named exports but at most one default, and `export default` takes a function or class declaration or an expression — so `export default const x = 1` is a syntax error.

go deeper

for a junior

Be ready to write both import shapes from memory and say which needs braces. Know that a default import name is yours to choose, while a named import must match the exported spelling exactly.

for a middle

Explain that export default just registers the export name "default", and show the equivalences: import { default as x }, export { x as default }, and ns.default after a namespace import. Know which combined import forms are grammatically legal.

for a senior

Show that unresolved import names are reported at link time rather than surfacing as undefined at runtime, and explain what that buys a codebase in early failure. Be able to justify a house rule on default exports for shared modules.

for a principal

Own the convention for the organisation: whether shared packages expose named exports only, how that interacts with barrel files and public API stability, and how you enforce it with lint rules rather than review habit.

## Two spellings, one mechanism Every ES module carries a table of export entries that map an **export name** (a string) to a binding inside the module. Named exports use whatever identifier you pick. `export default` is not a second mechanism bolted on beside it — it simply registers an entry under the literal name `"default"`. Almost every behaviour below falls out of that one fact. ## The named-export forms There are two spellings, and they are interchangeable: ```js // geometry.js export const PI = 3.14159; // declaration and export together export function area(r) { return PI * r * r; } export class Circle {} const helper = (x) => x * 2; export { helper }; // export clause, written separately export { area as circleArea }; // the same value under a second name ``` The export clause form is useful when you want the export list in one visible place, or when you want to expose a binding under a different public name than its internal one. A single binding may be exported under several names; each `as` adds another entry pointing at the same binding. ## The default form ```js // logger.js export default function log(msg) { console.log(msg); } export const LEVEL = 'info'; // defaults and named exports coexist ``` `export default` accepts a function declaration, a class declaration, or **an expression** — so `export default { retries: 3 }` and `export default 42` are both legal, while `export default const x = 1;` is a SyntaxError because a `const` declaration is not an expression. A module may declare at most one default; a second one is a SyntaxError. The clause equivalent is `export { helper as default }`. ## The import forms ```js import log from './logger.js'; // default, name chosen by the importer import whateverIWant from './logger.js'; // identical to the line above import { LEVEL } from './logger.js'; // named, must match exactly import { LEVEL as level } from './logger.js'; // named, renamed locally import log, { LEVEL } from './logger.js'; // default plus named import log, * as logger from './logger.js'; // default plus namespace import * as logger from './logger.js'; // logger.LEVEL, logger.default import './setup.js'; // no bindings, side effects only ``` One combination is *not* allowed: a namespace import cannot sit beside a named-import list, so `import { LEVEL }, * as logger from './logger.js'` is a SyntaxError. Note also that `import { default } from './logger.js'` fails, because `default` is a reserved word and cannot be a binding name — you must write `import { default as log }`. ## Mismatched names fail structurally, not silently Because imports are resolved before any module code runs, a named import that does not correspond to an export is reported as a SyntaxError at link time, before the importing module executes a single statement. It does not quietly produce `undefined`, and it does not wait until you call the value. The same applies to a default import from a module that declares no default: `default` is just a name, and an unresolvable name is an error. Candidates who expect `undefined` are carrying over intuition from destructuring a plain object. ## Which to reach for Named exports keep the public name of a value under the author's control: the importer must spell it correctly, editors can auto-import and rename it across a project, and the name is greppable. A default export lets a file present a single obvious thing — a component, a class, a configuration object — without repeating its name, but the naming decision moves to each importer, so the same value ends up called different things in different files. Many teams standardise on named exports for shared libraries for exactly that reason, while single-artifact files keep a default. Mixing both in one module is legal and common, but it forces consumers to use both import shapes for one file. ## A quick self-check Given `export default class Button {}` in `button.js`, `import Btn from './button.js'` and `import { default as Btn } from './button.js'` bind the same class, and `import * as m from './button.js'` gives an object whose `default` property is that class. If you can state that, you have the model right.

  • Can a module have both a default export and named exports at the same time?
    Yes. `export default class Button {}` alongside `export const SIZES = [...]` is completely legal, and a consumer can pull both in one statement: `import Button, { SIZES } from './button.js'`. The only cap is that there can be at most one default entry per module; named exports are unlimited.
  • Why is `export default const config = {}` a syntax error when `export const config = {}` is fine?
    Because `export default` is followed by a function declaration, a class declaration, or an expression — never a variable declaration. `const config = {}` is a declaration, so it does not fit the grammar. Write `const config = {}; export default config;`, or just `export default {}` if the local name is not needed.
  • What does `import './polyfills.js'` with no bindings actually do?
    It is a side-effect-only import: the module is resolved, linked and evaluated exactly once, but nothing is bound in the importing scope. It is the right form for a module whose whole purpose is to run — registering a polyfill, installing a global handler, importing a stylesheet in a bundler pipeline.

saying these in an interview costs you the question

  • Thinks a missing named import quietly yields undefined
  • Says default exports are a different mechanism from named exports
  • Believes a module can have several default exports
  • Claims named imports can be renamed but default imports cannot
  • Writes import { default } from './m.js' and expects it to parse

context

open as a page

In an ES module you write `var config = {}` and `function init() {}` at the top level. Are `globalThis.config` and `globalThis.init` defined afterwards, and how does that differ from the same two lines in a classic (non-module) script?

level: juniorimportance: must knowfreq 70%

basics

~10 s

No. Top-level declarations in an ES module live in that module's own scope, so globalThis.config and globalThis.init stay undefined. In a classic script the same declarations become properties of the global object.

open as a page

In an ES module `main.js` you write `console.log('main start');` and then, on the line below it, `import './dep.js';` — and `dep.js` logs `'dep evaluated'` at its top level. Which line prints first, and what rule decides that?

level: juniorimportance: must knowfreq 68%

basics

~20 s

The dependency prints first. Import declarations are hoisted and the whole module graph is linked before anything is evaluated, so dep.js runs to completion before any statement in main.js — including a statement written above the import.

open as a page

A module `counter.mjs` contains `export let count = 0;` and `export function increment() { count++; }`. Another ES module runs `import { count, increment } from './counter.mjs'; increment(); console.log(count);` — what is logged, and why?

level: middleimportance: must knowfreq 68%

basics

~20 s

It logs 1. An ES module import is a live, read-only binding to the exporter's own variable rather than a copy of its value, so reading count after increment() has run sees the exporter's updated value.

open as a page

An ES module contains no `'use strict'` pragma anywhere. Is its code strict, can you opt out, and does a function defined in that module stay strict when it is later called as a callback from non-strict code?

level: middleimportance: must knowfreq 60%

basics

~20 s

Module code is strict by definition, so the pragma is redundant and there is no way to opt out. Strictness is lexical, not caller-dependent: a function written inside a module stays strict wherever non-strict code later calls it.

open as a page

Why is `if (isDev) { import './debug.js'; }` a syntax error in an ES module, and why must the module specifier be a plain string literal rather than a variable or a concatenation?

level: middleimportance: must knowfreq 60%

basics

~20 s

ES module imports are static by design: an import declaration may appear only at the top level of a module, and its specifier must be a string literal. That lets the engine read the whole dependency graph out of the syntax and link it before running any code — which conditional or computed imports would make impossible.

open as a page

In an ES module you write `import { config } from './settings.js'` and later `config = {};`. What happens, and how is that different from writing `config.debug = true;`?

level: juniorimportance: should knowfreq 46%

basics

~20 s

Assigning to config fails: an imported name is a read-only binding, so the write is rejected with an error and never reaches the exporter. Setting config.debug = true works, because both modules point at the same object.

open as a page

In an ES module, what does `export * from './utils.js'` actually re-export, and what does it leave out?

level: middleimportance: should knowfreq 50%

basics

~10 s

It forwards every named export of ./utils.js under the same names, and deliberately excludes that module's default export. It also creates no local bindings, so the re-exporting file cannot use those names itself.

open as a page

In an ES module barrel file, what is the difference between writing `export { parse } from './parser.js'` and writing `import { parse } from './parser.js'; export { parse };`?

level: middleimportance: should knowfreq 38%

basics

~20 s

Consumers see the same export either way, but the from-form creates no local binding: the barrel itself cannot reference parse. The import-then-export version does bind parse in module scope, so the file can also use the value.

open as a page

At the top level of an ES module, what is the value of `this`, and what breaks in older library code written on the assumption that top-level `this` is the global object?

level: middleimportance: should knowfreq 52%

basics

~20 s

Top-level this in an ES module is undefined, not the global object. Library code that assigns its export onto this, or sniffs the environment through this, throws a TypeError or silently detects nothing once the file is loaded as a module.

open as a page

When designing an ES module's public surface, what are the practical tradeoffs between exposing values as named exports versus a single default export?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Named exports keep the public name under the author's control, so identifiers stay consistent, greppable and safely renameable by tooling. A default export makes a one-artifact file read cleanly but hands the naming decision to every importer, which invites drift.

open as a page

A module has `export let ready = false;` and sets `ready = true` during startup. Consumers using `import { ready }` see it become true, but after the same code is compiled to CommonJS and consumers write `const { ready } = require('./m')`, they see false forever. What explains the difference, and how would you export this value so it works under both?

level: seniorimportance: should knowfreq 38%

basics

~20 s

An ES module import is a live binding to the exporter's variable, while destructuring a CommonJS require copies the property's value at that instant. Export a function or a state object so consumers read the value at access time instead of capturing it.

open as a page

A JavaScript file that worked when loaded as a classic script now throws ReferenceError once the same code is loaded as an ES module — once on a helper function another file defines, and once on a line that assigns to a name without declaring it. What changed, and how do you fix each?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Two module rules bit at once. Module scope means the other file's helper is no longer visible as a bare name, so it must be exported and imported; implicit strict mode turns the undeclared assignment from a silent global into a ReferenceError, so declare it with const or let.

open as a page

A module counts how many times its top-level code runs and five different modules import it. How many times does the body actually run, what identifies a module instance, and how can a codebase end up with two separate instances of what looks like the same module?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Once. A module's body is evaluated exactly one time per instance, and instances are keyed by the resolved location of the module, so all five importers share one instance. Two instances appear when the same source resolves to two different locations — a duplicated package copy, a differing specifier, or a separate realm.

open as a page

In an ES module, `import * as ns from './m.js'` gives you `ns`. What kind of object is `ns` — can you add, delete or overwrite properties on it, and what is its prototype?

level: middleimportance: nice to knowfreq 31%

basics

~20 s

ns is a module namespace object: non-extensible, with one non-configurable property per export whose value is a live view of the exporter's variable. Adding, deleting or overwriting properties is rejected, and its prototype is null.

open as a page

Parts of a large codebase rely on module top-level side effects — plugins that self-register on import, a logger configured at import time — so correctness depends on which modules are imported and in what order. As the engineer setting the standard, how do you evaluate that pattern and what would you put in its place?

level: principalimportance: nice to knowfreq 30%

basics

~20 s

Treat evaluation-order dependence as hidden coupling: the ordering contract lives in import statements no reviewer reads as ordering. Prefer module bodies that only declare things, plus one composition root that constructs and wires everything in an order you can read, test, and change.

open as a page