Skip to content
ES Modules: import and export
step 1/7

Reading — step 1 of 7

Learn

~3 min readModern Syntax

Modern JavaScript code is split into modules — small files that explicitly export and import. ES Modules (ESM) are the official format, supported in browsers, Node.js, Deno, Bun.

Named exports

Export individual values:

javascript

Import with the same name(s):

javascript

Default exports

For modules that have one main thing to export:

javascript

Import with any name you like:

javascript

A module can have ONE default export AND multiple named exports:

javascript

Namespace import

Import everything as one object:

javascript

Useful when you want to be explicit about which module a function comes from.

Re-exports

A module can re-expose another module's exports:

javascript

Lets consumers import { add, Logger } from './index.js' instead of importing from many files.

Dynamic imports

Load a module at runtime:

javascript

Returns a Promise of the module namespace. Useful for code splitting (only load what's needed when it's needed) and conditional imports.

How modules differ from old scripts

  • Always strict mode'use strict' is implicit
  • Top-level scope is the module, not global — variables don't leak
  • this at top level is undefined, not the global object
  • Imports are hoistedimport statements are processed before any code runs, regardless of where they appear in the file
  • Imports are bindings, not copies — when the source updates a value, importers see the update
  • Cyclic imports are handled — at the cost of some complexity

Browser vs Node ESM

Browser: loaded via <script type="module" src="app.js"></script>. Always relative paths or full URLs. Extension required.

Node: activated by "type": "module" in package.json, or .mjs extension. Most file paths need extensions (.js).

Common mistakes

  • Mixing ESM and CommonJS — Node treats .js as CommonJS unless package.json says "type": "module". Mismatches cause cryptic errors.
  • Forgetting the file extension — Node ESM requires .js (not just ./util).
  • Trying to mutate imported bindings — read-only. Export functions/objects to allow mutation.
  • Treating import * as math as a deep-copyable object — it's a special namespace object; can't be JSON-serialized or freely modified.
  • Default + named together — the default binding comes first, then the braces: import myDefault, { named } from '...'. (default itself is a keyword, not a usable name — you always give the default export a local name of your own.)

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…