JavaScript has two ways to export from a file, and choosing well makes the rest of the project easier to navigate.
// Named — the one to prefer
export function formatDate(date) { ... }
export function parseDate(text) { ... }
// Default — one per file
export default function Button() { ... }Named exports#
// dates.js
export function formatDate(date) {
return date.toISOString().slice(0, 10);
}
export const DEFAULT_LOCALE = "en-GB";// app.js
import { formatDate, DEFAULT_LOCALE } from "./dates.js";
import { formatDate as format } from "./dates.js"; // rename on importThree real advantages:
- The name is fixed. Everyone imports
formatDate, so searching the project finds every use. - Your editor can autocomplete it and auto-import it, because it knows the name.
- Typos become errors. Importing
{ formatDat }fails loudly; a misnamed default import does not.
Default exports#
// Button.js
export default function Button({ label }) { ... }import Button from "./Button.js";
import AnythingAtAll from "./Button.js"; // also legal — no errorThat second line is the problem. The importer chooses the name, so the same function ends up called three different things across a codebase and nothing flags it.
Defaults still make sense when a file genuinely is one thing — a React component, a config object, a single class. Many teams use exactly that rule.
A sensible convention#
- Named exports by default. Especially for utilities, where a file holds several related functions.
- One default when the file is a single unit and the filename already names it.
- Do not mix both in one file unless there is a reason — it makes the import line inconsistent across the project.
Re-exporting#
// utils/index.js
export { formatDate, parseDate } from "./dates.js";
export { slugify } from "./text.js";import { formatDate, slugify } from "./utils/index.js";This is a “barrel file”. It tidies imports and has a real cost: it can pull in more code than you use and it makes circular imports much easier to create accidentally. Worth it for a public library boundary, usually not for internal folders.
ES modules versus CommonJS#
| ES modules | CommonJS | |
|---|---|---|
| Export | export function f() {} |
module.exports = { f } |
| Import | import { f } from "./a.js" |
const { f } = require("./a") |
| Where | Browsers and modern Node | Older Node code |
Write ES modules in new code. You will still meet require in older projects and plenty of tutorials.
Import order and side effects#
import "./styles.css"; // imported purely for its side effect
import { formatDate } from "./dates.js";Imports are hoisted and evaluated before the rest of the file runs, so a module’s top-level code executes on first import regardless of where the import line sits. Keep top-level side effects to a minimum — a module that opens a connection when imported is difficult to test.
Questions people ask#
Can I have both named and default exports?
Yes: import Button, { ButtonGroup } from "./Button.js". Legal, and inconsistent enough that many teams forbid it.
Why is my import undefined?
Usually a name mismatch — a named export imported as a default, or the other way round. Check whether the source file wrote export default.
Do I need the .js extension?
In the browser and in Node’s ES modules, yes. Bundlers often let you omit it, which is why code copied from a bundled project fails when run directly.
What is tree shaking?
A bundler removing exports nothing imports. It works reliably with named exports and is less effective through barrel files.