JavaScript Modules Table

SyntaxWhat it doesField note
export const x = 1Named exportThe declaration stays live - importers see updates
export default valueModule's main exportOne per module; imported with any name you choose
export { a as b }Rename on the way outPublic API name decoupled from internal name
import { x } from 'm'Named importBraces and name must match the export exactly
import x from 'm'Default importNo braces; the name is local - conventions say x = module name
import * as m from 'm'Namespace importEverything named, on one object; defaults ride m.default
import('m')Dynamic importReturns a promise - code-splitting and lazy routes live here
export * from 'm'Re-exportBarrel files (index.js) are built from this
<script type="module">Turn a script into a moduleStrict mode + own scope + deferred by default
import mapsMap specifiers to URLsLets bare names like 'lodash' work in the browser
Reference: the MDN modules guide. The design choice every codebase makes: default export = the module is one thing (a component, a class); named exports = the module is a collection of utilities. Mixing both is normal, but a module with twelve named exports and no default is easier to tree-shake and harder to misname. Bottom line: modules are deferred and strict by default - code inside one runs after the DOM is parsed, which quietly retires the old DOMContentLoaded boilerplate. Related tools: JS promise table (dynamic import returns one), JS error table (module resolution failures), and Map and Set table for module-level state.

ES modules are JavaScript's built-in packaging: each file is a module with its own scope, imports and exports form a dependency graph, and the browser loads that graph without any tooling. The table below is the complete working syntax - ten forms that cover everything from a first import statement to barrel files and code-splitting.

Bottom line: the default-vs-named choice is a design statement. A default export says this module IS one thing - a component, a class, a single function; the importer names it, so conventions carry the meaning. Named exports say this module is a collection of utilities; the names are fixed, refactoring-safe, and tooling can tree-shake them precisely. Most mature codebases lean named, keeping default for the one obvious thing a module exists to provide.

The honest part: two behaviors surprise people coming from CommonJS. Module imports are LIVE bindings - if the exporting module changes a value, importers see the update, which is nothing like a copied require(). And everything inside a type="module" script is deferred and strict by default: it runs after the DOM is parsed, which quietly retires the window-onload boilerplate an entire generation memorized.

How to use

  1. Match the direction you are going: export rows define a module's surface, import rows consume someone else's.
  2. Pick default vs named by the design rule in the note - one obvious thing defaults to default; utilities default to named.
  3. Use dynamic import() when the timing matters: routes, modals, and heavy libraries load only when first needed.

Frequently asked questions

When should I use export default versus named exports?

Default marks the module's single identity: Button.js exports the Button, dateUtils.js would export a default only if one function dominated. Named exports mark collections: api.js exporting fetchUser, deleteUser, updateUser is better served by three names than one ambiguous default object. Two practical tiebreakers: named imports cannot be misnamed (the compiler checks the spelling), and bundlers tree-shake unused named exports more reliably - a default object with twelve methods ships all twelve.

What is a barrel file and when does it hurt?

A barrel is an index.js that re-exports a folder's modules (export * from './user'). It gives importers one short path - import { User } from './components'. The cost appears at scale: every barrel entry loads on import unless your bundler can tree-shake through it, and circular references between barrels and modules produce the infamous undefined-at-import bug. Small folders: barrel freely. Large apps: import from the real paths, or keep barrels per-feature rather than one global one.

What are circular imports and how do I fix them?

Module A imports B while B imports A. ES modules handle this without crashing - each module sees the other's LIVE bindings - but whichever module executes first reads undefined for values the other has not initialized yet. The symptom is a class or function being undefined exactly once at startup. Fixes, in order of preference: extract the shared piece into a third module both import; pass the dependency in as a parameter; or defer the use to call time rather than module-evaluation time - live bindings mean late access works.

Why does import() take a promise while import does not?

Static imports are resolved before your code runs - the whole graph loads, links, and evaluates first; there is no moment where a module is missing. import() is runtime: it fetches and evaluates the module on demand, so the request itself is asynchronous and returns a promise of the module namespace. That asynchrony is the feature: routes, modals and heavy editors wait for import() until first use, and bundlers split each dynamic import into its own downloadable chunk - the entire code-splitting mechanism in one line.

Related tools