Modules, packages, and the toolchain · Lesson 61 of 77 · practice

Named and default exports

About 55 minutes.

By the end of this lesson you can

  • Tell a named export from a default export by looking at the export line.
  • Write the import line that matches each kind, including renaming with as.
  • Explain who chooses the name a default export arrives under, and why.
  • Say what a namespace import gives you and where the default sits inside it.
  • Publish a mixed surface - one main tool plus named helpers - in a runnable stand-in.

Two ways to push something through the hatch

Explanation

Last lesson used one kind of export: named. You put a name through the hatch, the name is part of the deal, and the importing file asks for it by that name. Most exports in most projects are named exports, and if you only ever learn this one you will be fine.

There is a second kind: the default export. A module may have at most one, and it means "this is the main thing this file is for". A file called date-formatter.js whose whole job is one function is the natural case. The syntax is export default followed by the thing: export default function labelFor(text) { ... }.

The two kinds live happily in the same file. A module can have one default and any number of named exports, which is exactly how a lot of small libraries are built: the headline tool as the default, the sharp knives as named exports beside it.

With a default export, the importer picks the name

Explanation

This is the difference that actually matters in practice, and it is not about syntax. A named export travels with its name: export function capitalise means every importing file writes capitalise, and if they want a different word locally they have to say so explicitly. A default export travels without one. The importing file writes import anythingIWant from "./format.js" and that is that.

Read that as a trade rather than as a convenience. Named exports give you one shared vocabulary across the whole project: search for capitalise and you find every use of it. Defaults give you freedom at each call site, and the cost is that the same function can be called label in one file, format in another and f in a third. Two years in, that cost is usually larger than the convenience, which is why plenty of teams have a rule of named exports only.

One consequence worth knowing before it bites: the name after export default is for humans and for stack traces, not for importers. Writing export default function labelFor does not oblige anybody to call it labelFor. Writing export default function () with no name at all is legal too, and it is a small act of sabotage against whoever reads the error report.

The import line has to match the export kind

Explanation

Named exports are imported inside braces: import { capitalise, DASH } from "./format.js". The braces mean "pick these names out". A default export is imported with no braces: import labelFor from "./format.js". Mixing them in one line is normal and reads default-first: import labelFor, { capitalise } from "./format.js".

To use a different local name for a named import, add as: import { capitalise as toTitleCase } from "./format.js". The same keyword works on the way out too - export { capitalise as toTitleCase } - which is how a module can publish a tidy public name for a scruffier internal one.

The single most common error in this area comes from mixing the two forms up. import { labelFor } from "./format.js" when labelFor is the DEFAULT export does not throw a helpful "wrong kind" message; you asked for a named export called labelFor and there isn't one, so you get undefined or a build error about a missing export. If an import arrives undefined and you are sure the file exports it, the braces are the first thing to check.

MDN — export

Check there: Named and default exports are documented as distinct forms, renaming with 'as' is shown for both directions, and duplicate names or more than one default export are described as a SyntaxError that prevents the module being evaluated.

The namespace import, and where the default hides

A mental model

There is a third import form: import * as format from "./format.js". It gives you a single object with every export of that module hanging off it, so you write format.capitalise(...) instead of importing each name. It is useful when a module has many exports and you want the reader to see where each one came from.

That object is the key to a much better mental model of the whole system. A module is not a bag of loose names - it is one object of named things, and importing is picking properties off it. And the default export is not magic either: on that object it is a property called default. MDN is explicit that import labelFor from "./format.js" is shorthand for import { default as labelFor } from "./format.js".

That is the model this lesson's runnable code leans on, and it is not an approximation. Our stand-in factories return an object whose keys are the module's exports, with the main tool under the literal key default - which is exactly what the real namespace object looks like. When you destructure it you have to write const { default: labelFor } = kit, renaming as you go, for the same reason the real import syntax needs as: default is a reserved word and cannot be a variable name.

MDN — JavaScript modules

Check there: The guide states that importing a default is shorthand for importing the binding named default, and describes the namespace object created by 'import * as'.

So which should you use?

Why it matters

A rule of thumb that will not embarrass you: name everything, and add a default only when a file genuinely has one headline export that callers will want to rename to suit their context.

The reasons are all about the reader rather than the writer. A named export is greppable, spell-checked by your tools, and consistent across every file that uses it. It is also easier for a bundler to remove when nobody uses it - the tree shaking of lesson five works on names it can see.

The reason not to be dogmatic: whole ecosystems disagree with the rule of thumb, and you will import plenty of packages whose main entry point is a default export. Knowing both forms and being able to read either is the skill. Having an opinion about which to write is a team decision, and a small one.

The mistakes to expect

A common mistake

Two defaults in one file. A module gets one. Writing export default twice is a syntax error and the module will not evaluate at all - which is at least a fast failure.

Assuming the file name binds the default's name. It does not. date-utils.js can export default a function called anything, and importers can call it anything else again. Nothing anywhere enforces a match.

export default { a, b, c } as a way of "exporting several things". It works, but you have just made one anonymous object export instead of three named ones: importers cannot pick out only b, and a bundler cannot drop the parts nobody uses. If you want several things, export several things.

Renaming the wrong way round. In import { capitalise as toTitleCase }, the name on the LEFT is the one the other module published, and the one on the right is what you will type locally. Getting this backwards produces an error naming an export that has never existed, which reads confusingly until you know the order.

Recap

Recap

Named exports carry their names to every importer and are imported inside braces. A default export is the module's single headline value, imported without braces and named by whoever imports it. Both can live in one file. Renaming uses as, with the source name on the left. import * as gives you the module's namespace object, on which the default sits under the key default - so a default import is shorthand for taking that one property. Prefer named exports for greppability and tree shaking; read both fluently, because the ecosystem uses both.

Worked examples

Every output below was produced by actually running the code in the same sandbox your exercises run in — it is not a prediction.

One module, one default, two named exports

Every form this lesson teaches, in one pair of files: an inline named export, a named constant, a default, a mixed import with a rename, a namespace import, and the line that proves the default is just a property called default.

1// --- file: src/format.js ----------------------------------------2export function capitalise(text) {3  return text.charAt(0).toUpperCase() + text.slice(1);4}5 6export const DASH = "-";7 8export default function labelFor(text) {9  return capitalise(text.trim());10}11 12// --- file: src/report.js ----------------------------------------13import labelFor, { capitalise, DASH as SEPARATOR } from "./format.js";14import * as format from "./format.js";15 16console.log(labelFor("  release notes "));17console.log(capitalise("novus") + SEPARATOR + "learn");18console.log(format.default === labelFor);
  • line 2

    A named export written inline. The word capitalise is now part of this module's contract - rename it here and every importing file breaks.

  • line 6

    Exports are not only functions. Constants, objects and classes all travel the same way.

  • line 8

    The single default. 'labelFor' here is for stack traces and for the reader; no importer is obliged to use it.

  • line 13

    Default first with no braces, then named ones inside braces. 'DASH as SEPARATOR' renames on the way in: published name on the left, local name on the right.

  • line 14

    The namespace form. One object, every export on it, and it is perfectly normal to import the same module twice in two different ways.

  • line 18

    Prints true. The default export is a property named default on the namespace object, which is why 'import labelFor from ...' is shorthand for 'import { default as labelFor } from ...'.

This one does not run hereThis is module syntax, and the runner on this page has no module loader. It is here to be read line by line; the runnable example below models the same public surface with a factory function.

The same surface as a plain object, running here

The factory returns an object with the main tool under the key default and a helper beside it - the same shape a namespace object has. Line 15 shows why the real import syntax needs 'as': default is a reserved word, so you must rename it to use it.

1function createDateKit() {2  function isoDay(year, month, day) {3    return year + "-" + month + "-" + day;4  }5 6  function shortMonth(name) {7    return name.slice(0, 3);8  }9 10  return { default: isoDay, shortMonth };11}12 13const dateKit = createDateKit();14 15const { default: formatDay, shortMonth } = dateKit;16 17console.log(formatDay("2026", "07", "31"));18console.log(shortMonth("September"));19console.log(Object.keys(dateKit).join(", "));20console.log(typeof dateKit.default);
  • line 10

    'default' is an ordinary property key - reserved words are allowed as property names. This is not a trick we invented; it is what the real module namespace object looks like.

  • line 15

    Destructuring with a rename. You cannot write 'const { default } = dateKit' because default is a reserved word and cannot be a variable name - the exact reason 'import { default as x }' needs the as.

  • line 17

    Once renamed, it is an ordinary function under an ordinary local name, chosen here rather than by the kit. That is the freedom a default export gives, and the inconsistency it risks.

  • line 19

    The published surface, listed. Real namespace objects sort their keys; this one keeps insertion order, which is the one detail the stand-in gets differently.

What it prints

2026-07-31Sepdefault, shortMonthfunction

Check yourself

Not scored, not stored. Getting one wrong is the useful part.

format.js contains 'export default function labelFor(text) { ... }'. Another file writes 'import makeLabel from "./format.js"'. Is that legal, and what is the function called there?

A plain object stands in for a module's namespace: the main tool under the key default, a helper beside it. Both are renamed as they are destructured. Predict all three lines.

const toolkit = {  default: function (text) {    return text.length;  },  upper: function (text) {    return text.toUpperCase();  },};const { default: measure, upper: shout } = toolkit;console.log(measure("modules"));console.log(shout("modules"));console.log(Object.keys(toolkit).join(","));

How many default exports may one module have?

Write it yourself

Exercise · core · 8 tests

Publish a default beside two named tools

The starter code publishes two named tools - capitalise and slug - but no default. Add one. Write a function labelFor(text) that trims the text and then capitalises it, and publish it on the returned object under the key 'default', beside the two that are already there. The finished kit models a module with one default export and two named exports, and the tests import it both ways: straight off the object, and destructured with a rename, the way real code takes a default.

What your code must do

createLabelKit() returns an object with exactly three keys: default, capitalise and slug, all functions. default(text) trims the surrounding spaces then uppercases the first character, leaving the rest of the text alone. capitalise(text) uppercases the first character only and returns "" for empty text. slug(text) trims, lowercases and replaces each single space with a hyphen. Nothing prints anything.

Define createLabelKit at the top level of your code — the tests call it by name.

Runs on your device in a sandbox with no network and no access to this page.

Worth remembering

  • Who decides the local name of a default export?

    The importing file. The name written after 'export default' is only for humans and stack traces; importers write any name they like.

  • Where does the default export live on a namespace object?

    Under the key 'default'. That is why a default import is shorthand for 'import { default as name }', and why the rename is compulsory.

  • In 'import { a as b } from ...', which name did the other module publish?

    a, on the left. b is the local name you will type in this file. The same order applies to 'export { a as b }'.

Check this lesson against the source

We wrote the explanation; we did not invent the facts. This is the page that backs them.

MDN — export

Check there: Named exports, the default export, the 'export { a as b }' rename form, and the rule that more than one default export is a SyntaxError that prevents the module from being evaluated.

MDN — import

Check there: The default, named, namespace and mixed import forms, and that the namespace object exposes the default export under the key 'default'.

MDN — JavaScript modules

Check there: A default import is described as shorthand for importing the binding named default, and only one default export is allowed per module.

Prefer to read the source in a structured breakdown? Turn this doc into a breakdown

0 of 1 exercise in this lesson solved on this device

Consent version 2026-07-31.1

Cookie preferences