Skip to main content

JavaScript modules: import/export

export sends a variable, function or class out of a module, and import pulls an exported entity into another file. Together they form the built-in module system that arrived with ES6 (2015) and lets you split code into module files and share values, functions, classes and so on between them.

Theory

TL;DR

  • export sends an entity outward, import loads it into another module.
  • A file may have any number of named exports; they are imported inside braces and under exactly their own names.
  • export default is allowed once per file, is imported without braces and under any name.
  • Renaming works both on export and on import through as, and import * as ns collects the whole module into one object.
  • Imports are static: you cannot write them inside if, for or a function; for on-demand loading there is await import().
  • A module is loaded and cached once, and all of this only works in a module context (type="module" in HTML, or .mjs or "type": "module" in Node.js).

Quick example

File math.js:

javascript
export const PI = 3.14; export function add(a, b) { return a + b; }

File app.js:

javascript
import { PI, add } from './math.js'; console.log(PI); // 3.14 console.log(add(2, 3)); // 5

Named export and default export

A named export (export) lets you send several entities out of one file.

javascript
// math.js export const PI = 3.14; export function multiply(a, b) { return a * b; }

They must be imported by name, inside braces, and the names have to match:

javascript
import { PI, multiply } from './math.js';

A default export (export default) sends out the one main entity of the file, most often the "main" object, function or class.

javascript
// math.js export default function add(a, b) { return a + b; }

It can be imported under any name and without braces:

javascript
import sum from './math.js'; console.log(sum(2, 3)); // 5

A comparison of the two forms:

Criterionexport (named)export default
Count per fileAs many as you likeExactly one
ImportInside braces {}Without braces
NameMust matchCan be anything
Renaming on importThrough asYou simply write another name
Exampleimport { add } from './file.js'import add from './file.js'

The three typical combinations:

javascript
// math.js: named export export const PI = 3.14; export function add(a, b) { return a + b; }
javascript
// app.js import { PI, add } from './math.js'; console.log(add(1, 2)); // 3
javascript
// math.js: default export (the function may be anonymous) export default function(a, b) { return a * b; }
javascript
// app.js import multiply from './math.js'; console.log(multiply(2, 3)); // 6
javascript
// math.js: mixed export export const PI = 3.14; export default function add(a, b) { return a + b; }
javascript
// app.js import add, { PI } from './math.js'; console.log(add(1, 2), PI); // 3 3.14

Renaming, namespace import and re-export

An entity can be renamed on export:

javascript
function sum(a, b) { return a + b; } export { sum as add };

and on import:

javascript
import { add as plus } from './math.js'; plus(2, 3); // 5

The whole module can be loaded as a single object, which is handy when there are many exports:

javascript
import * as math from './math.js'; console.log(math.PI); // 3.14 console.log(math.add(2, 3)); // 5

A re-export lets one module repackage others, for example to give a folder a single entry file:

javascript
// utils.js export { add, multiply } from './math.js';

Now the consumer takes everything from one place:

javascript
import { add } from './utils.js';

How modules behave

FeatureDescription
Works only in modules (type="module" in HTML, or .mjs or "type": "module" in Node.js)Otherwise you get a SyntaxError
Import and export are always staticThey cannot be written inside if, for or a function
Modules are cachedLoaded once and reused afterwards
Module loading is asynchronousFor on-demand loading there is await import()
A module body runs in strict mode and has its own scopeModule variables never land on the global object

Named imports are live bindings: if the module later changes an exported variable, the consumer sees the new value rather than a copy of the old one.

A dynamic import loads a module on demand and returns a promise:

javascript
const math = await import('./math.js'); console.log(math.add(2, 3)); // 5

It works inside an async function or at the top level of a module, and it is exactly what code splitting is built on.

ES modules versus CommonJS

CriterionES modules (import / export)CommonJS (require / module.exports)
Syntaximport x from './m.js'const x = require('./m.js')
When links are resolvedStatically, before executionDynamically, at run time
Where it worksBrowser and Node.jsHistorically Node.js
LoadingAsynchronous, with await import()require is synchronous
What an import givesLive bindings to the exported valuesA copy of the module.exports object at call time
Tree shakingPossible thanks to static analysisPractically impossible
javascript
// CommonJS const { add } = require('./math.js'); module.exports = { add }; // ES modules import { add } from './math.js'; export { add };

Summary

KeywordWhat it doesExample
exportSends variables and functions outwardexport const PI = 3.14;
export defaultSends out the main entity of the fileexport default class User {}
importWires in the exported entitiesimport { PI } from './math.js';

Put simply:

export means "send outward", import means "load inward", and export default means "send the one main thing".

Common mistakes

  1. Forgetting the module context. A plain <script src="app.js"> does not understand import; you need <script type="module" src="app.js">, and in Node.js either the .mjs extension or "type": "module" in package.json.
  2. Mixing up the braces. import { add } from './math.js' takes a named export, import add from './math.js' takes the default. If the module has no default, the second form yields undefined or a bundler error.
  3. Getting a named import's name wrong. Names must match character for character; to use another name you need as: import { add as plus } from './math.js'.
  4. Writing several export default statements in one file. Exactly one is allowed, otherwise it is a SyntaxError.
  5. Putting import inside a condition or a function. A static import is only valid at the top level of a module; for conditional loading there is await import().
  6. Dropping the file extension in a relative path. In the browser and in Node.js with ES modules './math' does not resolve, you need './math.js', unlike the habit from CommonJS and bundlers.
  7. Expecting a module to run twice. Modules are cached: no matter how many files import it, the module body executes once.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.