JAVASCRIPT / MODULES AND DEVELOPER TOOLING
Exporting values with named and default exports
Publish a module's public surface with named exports, renamed exports, and a single default, and predict which exported values stay live.
What you will learn
- Publish names inline with export const/function or in one export { a, b } clause
- Rename at the export site with export { internalName as publicName }
- Treat export default as the export named default, with the importer picking the name
- Predict which exports update later: live bindings versus a default snapshot
Understanding Exporting values with named and default exports
A module does not return anything. It publishes a list of names, and export is how a top-level binding gets onto that list. The engine builds the list while parsing, before a single line of the module body runs, which is why export cannot sit inside a function or an if block and why the exported name must be a literal identifier rather than a computed string. That early list is also what makes a misspelled import fail before any code executes instead of handing you undefined much later.
export default is not a separate mechanism. It is a named export whose name is the reserved word default, and the braceless import form is shorthand for requesting that one name while letting the importing file choose the local identifier. That is the whole difference: with named exports the module decides the name and the importer has to match it, while with the default the module says nothing about naming.
What crosses the module boundary is a binding, not a copied value. If a module exports a let and later reassigns it, every importer reads the new value because they all share one binding, and they share it read-only, so only the owning module can change it. The default is the exception worth memorising: export default followed by an expression evaluates that expression once and stores the result under the name default, so reassigning the variable you handed to it changes nothing for importers.
// ==== file: temperature.mjs ====
export const FREEZING_C = 0;
export function toFahrenheit(celsius) {
return celsius * 9 / 5 + 32;
}
// At most one default per module; the importer names it.
export default function describe(celsius) {
return celsius + 'C is ' + toFahrenheit(celsius) + 'F';
}
// ==== file: main.mjs ==== run with: node main.mjs
import describe, { FREEZING_C, toFahrenheit } from './temperature.mjs';
console.log(FREEZING_C);
console.log(toFahrenheit(100));
console.log(describe(20));A module's exports are a statically known list of names attached to live bindings, and default is just one more name on that list.
Worked examples
default is a name, not a category
Shows that export default log and export { log as default } produce the identical export.
// ==== file: logger.mjs ====
function log(message) {
console.log('[log] ' + message);
}
const LEVEL = 'debug';
// Same effect as: export default log;
export { log as default, LEVEL };
// ==== file: app.mjs ====
import anyNameIWant, { default as sameFunction, LEVEL } from './logger.mjs';
anyNameIWant('started');
console.log(anyNameIWant === sameFunction);
console.log(LEVEL);Example explained
Line 1export { log as default, LEVEL } places log on the export list under the name default, which is exactly what export default log does.
Line 2The braceless import asks for the export named default, so app.mjs is free to invent the local name anyNameIWant.
Line 3default as sameFunction reaches the same export slot through named-import syntax, so the identity check prints true.
live named export against a snapshotted default
Demonstrates that an exported let tracks later reassignment while export default of an expression does not.
// ==== file: counter.mjs ====
export let clicks = 0;
export function click() {
clicks += 1;
}
export default clicks; // stores the value 0 under the name 'default'
// ==== file: app.mjs ====
import snapshot, { clicks, click } from './counter.mjs';
console.log(clicks, snapshot);
click();
click();
console.log(clicks, snapshot);Example explained
Line 1export let clicks publishes the binding, so the imported clicks re-reads the module's current value on every access.
Line 2click() reassigns clicks inside counter.mjs, and the second log in app.mjs therefore reports 2.
Line 3export default clicks was evaluated once while the module ran, so snapshot holds the number 0 permanently.
Line 4app.mjs cannot assign to clicks itself; imported names are read-only views of the exporter's binding.
renaming and re-exporting to shape a public surface
Uses as in an export clause and a re-export to expose different names than the internals use.
// ==== file: internal-math.mjs ====
export function addNumbers(a, b) {
return a + b;
}
export function roundTo(value, digits) {
return Number(value.toFixed(digits));
}
// ==== file: math.mjs ====
export { addNumbers as add, roundTo as round } from './internal-math.mjs';
export const VERSION = '1.0.0';
// ==== file: app.mjs ====
import { add, round, VERSION } from './math.mjs';
console.log(add(2, 3));
console.log(round(3.14159, 2));
console.log(VERSION);Example explained
Line 1export { addNumbers as add } from './internal-math.mjs' forwards the binding without creating a local add inside math.mjs, so the name exists only for consumers.
Line 2Because the rename happens in the export clause, internal-math.mjs can rename addNumbers later and only this one line has to change.
Line 3math.mjs declares no default, so import anything from './math.mjs' would fail to link even though every file is valid JavaScript.
Important notes
A module may have at most one default. A second export default, or an export { x as default } next to an existing one, is a duplicate-export SyntaxError.
export only exists in module context: an .mjs file, a package.json type field set to module, or a script tag with type=module. Also, an anonymous default like export default () => {} ends up with the function name default.
Common mistakes
Writing export default const config = {}: default accepts an expression or a function/class declaration, not a let/const declaration, so the file is a SyntaxError. Declare the const first, then export default config.
Treating the export clause as an object literal, as in export { name: value }: the braces list existing bindings, not key/value pairs, so the module never parses.
Packing everything into export default { add, sub } and then writing import { add } from './math.mjs': the module has exactly one export, named default, so linking fails with no export named add and nothing in either file runs.
Try it yourself
Change, predict, then run
In a two-file module sandbox, write shapes.mjs with a default function area(r), a named constant TAU, and an exported let callCount that area increments. From main.mjs, log callCount, call area twice, and log it again to prove the named export is live.
Open the JavaScript workspaceCheck your understanding
A module contains: export let total = 0; export function add(n) { total += n; } export default total; A consumer imports the default as snapshot plus the named total and add, calls add(5) twice, then logs total and snapshot. It prints 10 and 0. Why do the two numbers differ?
- Named exports are hoisted before the default export runs, so the default saw total before add was defined.
- The default export is copied into each importing file, while named exports are re-read from the module on every access.
- export default evaluated the expression total once during module evaluation and stored that value under the name default, while the named export shares the module's live binding.
- They should not differ; both names point at the same variable, so the example must be using a bundler that breaks live bindings.
Show answer
The default export entry holds the result of evaluating its expression a single time while the module runs, so snapshot is frozen at 0, whereas total is a shared binding and reads 10 after two calls. The 'copied into each importer' option is close but puts the copy in the wrong place: the value is captured in the exporting module when the export default line executes, and making a default track later changes requires exporting a declaration or a function that reads the variable.