JavaScript export and import
Learn JavaScript export and import: named exports, default exports, renaming with as, namespace and dynamic imports, and re-exporting.
In modern JavaScript, the export and import keywords are how you split a program across many files and then stitch it back together. A file that uses them is an ES module: instead of dumping everything into one giant script, you keep each piece of functionality in its own file, declare what it shares with the outside world (export), and pull in only what you need elsewhere (import).
This guide covers every form you will meet in practice — named exports, default exports, renaming with as, namespace imports, re-exporting, and dynamic import() — and the gotchas that trip people up. Each example is small and runnable.
Why modules at all?
Before ES modules, sharing code meant attaching things to the global object and hoping names did not collide. Modules fix this:
- Encapsulation — anything you do not export stays private to the file.
- Explicit dependencies — the
importlines at the top of a file are a precise list of what it relies on. - One-time evaluation — a module's top-level code runs once, no matter how many files import it; the result is cached and shared.
To run modules in the browser, load your entry file with type="module":
<script src="app.js" type="module"></script>In Node.js, either name files .mjs or set "type": "module" in package.json.
Named exports
A named export ships a binding under its own name. A module can have as many named exports as you like.
// math.js
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
// You can also list exports at the bottom:
const subtract = (a, b) => a - b;
export { subtract };You import named exports inside curly braces, using the exact same names:
// app.js
import { add, PI } from './math.js';
console.log(add(2, 3)); // 5
console.log(PI); // 3.14159The names must match the export. import { Add } when the export is add throws a SyntaxError — there is no such named export.
Renaming with as
When two modules export the same name, or a name is awkward, rename on the way in (or out) with as:
// Rename on import
import { add as sum } from './math.js';
console.log(sum(1, 1)); // 2// Rename on export
const internalAdd = (a, b) => a + b;
export { internalAdd as add };Default exports
A default export is the single "main" thing a module provides. A module can have at most one default export.
// multiply.js
export default function multiply(a, b) {
return a * b;
}When importing a default, you do not use curly braces, and you choose the local name yourself:
// app.js
import multiply from './multiply.js'; // any name works
console.log(multiply(4, 5)); // 20Because the importer names it, default exports are often used when a file represents one thing — a single class, a single React component, a single config object.
Mixing default and named exports
A module can have both. The default goes outside the braces, named ones inside:
// user.js
export default class User { /* ... */ }
export const ADMIN_ROLE = 'admin';// app.js
import User, { ADMIN_ROLE } from './user.js';Prefer named exports by default. They make imports self-documenting and let tooling (autocomplete, "find references", tree-shaking) work reliably. Reach for a default export only when a file truly exports one thing.
Importing techniques
Namespace import (import * as)
To grab everything a module exports as a single object, use a namespace import:
// app.js
import * as math from './math.js';
console.log(math.add(5, 3)); // 8
console.log(math.PI); // 3.14159Every named export becomes a property of math. A default export, if any, appears as math.default. Use this when a module groups many related helpers.
Dynamic import (import())
The import statement above is static — it runs before anything else and its path must be a string literal. Sometimes you want to load a module on demand: only when the user opens a feature, or based on a runtime condition. For that, use import() as a function. It returns a Promise that resolves to the module's namespace object:
// Load a heavy module only when needed
async function openEditor() {
const editor = await import('./editor.js');
editor.init();
}Dynamic import works inside functions and conditionals — places a static import is not allowed — which makes it the basis of code-splitting and lazy loading.
Re-exporting
A "barrel" file can collect exports from several modules and forward them, so consumers import from one place:
// shapes/index.js
export { Circle } from './circle.js';
export { Square } from './square.js';
export { default as Triangle } from './triangle.js'; // re-export a default as a name// app.js
import { Circle, Square, Triangle } from './shapes/index.js';Common gotchas
- Extensions matter in the browser and in Node ESM.
import { x } from './math'fails; write'./math.js'. import/exportonly work at the top level of a module — not inside anifblock or a function. Use dynamicimport()for conditional loading.- Imports are read-only live bindings. You can read an imported value but not reassign it;
add = 5on an import throws. - A default export is not magically named.
export default addexports the value, not the nameadd; the importer picks the local name. - Modules are deferred and run in strict mode automatically — no need for
'use strict'.
A worked example: a mini library system
Here several modules each export an array of book objects, and a main file imports and combines them.
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Interactive Library App</title>
</head>
<body>
<h1>Interactive Library App</h1>
<button id="loadFiction">Load Fiction</button>
<button id="loadSciFi">Load Sci-Fi</button>
<div id="bookList"></div>
<script src="app.js" type="module"></script>
</body>
</html>fiction.js:
export const fictionBooks = [
{ title: 'Pride and Prejudice', author: 'Jane Austen' },
{ title: 'To Kill a Mockingbird', author: 'Harper Lee' }
];sciFi.js:
export const sciFiBooks = [
{ title: 'Dune', author: 'Frank Herbert' },
{ title: 'Neuromancer', author: 'William Gibson' }
];app.js:
import { fictionBooks } from './fiction.js';
import { sciFiBooks } from './sciFi.js';
function displayBooks(books) {
const list = document.getElementById('bookList');
list.innerHTML = '';
books.forEach(book => {
const item = document.createElement('div');
item.textContent = `${book.title} by ${book.author}`;
list.appendChild(item);
});
}
document
.getElementById('loadFiction')
.addEventListener('click', () => displayBooks(fictionBooks));
document
.getElementById('loadSciFi')
.addEventListener('click', () => displayBooks(sciFiBooks));Each category lives in its own module and exports a function or data, and app.js brings them together. Splitting data and behavior this way keeps each file small and makes new categories trivial to add.
Best practices
- Favor named exports so imports document themselves and tree-shaking works.
- One responsibility per module — a file should be easy to describe in a sentence.
- Use barrel files sparingly — they tidy imports but can hurt tree-shaking if overused.
- Lazy-load heavy or rarely used code with dynamic
import(). - Always include file extensions in relative paths.
Conclusion
Named exports give you many bindings per file, default exports give you one "main" export, and import (with as, * as, and dynamic import()) gives you precise control over what comes in and when. Together they turn a pile of scripts into a maintainable, tree-shakeable module graph. Next, see how modules are loaded in the browser in Modules, introduction.