JavaScriptimport / export

import / export

import and export are the two halves of the ES module system. export declares what a file makes available; import pulls those values into another file. The syntax is small, but it has a few interesting properties that catch developers off guard — bindings are live, the structure is static, and there is a quiet difference between named exports and the single default export.

Named exports

A file can have any number of named exports. The exported name is the variable name (or you can rename on the way out).

utils.js

JS
// Inline form — export at the point of declaration.
export const VERSION = "1.0.0";

export function greet(name) {
  return "Hello, " + name;
}

export class Box {
  constructor(value) { this.value = value; }
}

// Or list everything at the bottom — same effect.
const PI = 3.14159;
function area(r) { return PI * r * r; }
export { PI, area };

// Rename on the way out.
function _privateName() { return "secret"; }
export { _privateName as publicName };

app.js

JS
import { VERSION, greet, Box, area, publicName } from "./utils.js";

console.log(VERSION);          // "1.0.0"
console.log(greet("Ada"));     // "Hello, Ada"
console.log(area(2));          // 12.56636
console.log(publicName());     // "secret"

// Rename on the way in if a name collides.
import { greet as sayHi } from "./utils.js";
console.log(sayHi("Lin"));
The default export

Each module may also export one anonymous default. It is conventionally used when the file is "about" a single thing — a React component, a class, a config object.

logger.js

JS
export default function log(message) {
  console.log("[log]", message);
}

// You can also have named exports alongside the default.
export const LEVEL = "info";

app.js

JS
// Default imports do not use braces, and you choose the name yourself.
import log, { LEVEL } from "./logger.js";

log("hello");          // [log] hello
console.log(LEVEL);    // info
Default exports are a name-free zone
The importer picks the local name — there is no shared spelling. Tools cannot autofix typos. Many style guides prefer **named exports only** for this reason: every importer uses the same identifier and `grep` finds every call site.
Namespace imports

If you want every named export grouped under one object, use * as:

app.js

JS
import * as utils from "./utils.js";

console.log(utils.VERSION);
console.log(utils.greet("Ada"));
// utils.default — if utils.js has a default export, it appears here.

This is handy when you only need one or two functions occasionally and don't want to maintain the import list. It also makes the source of each value obvious at the call site.

Re-exporting (barrels)

A module can forward exports from another module. This is how "barrel" files (the conventional index.js that re-exports a folder) are built.

components/index.js

JS
export { Button } from "./Button.js";
export { Card }   from "./Card.js";

// Rename on the way through.
export { default as Dialog } from "./Dialog.js";

// Re-export everything (named) from another file.
export * from "./icons.js";

// Re-export everything as a sub-namespace.
export * as forms from "./forms.js";

app.js

JS
import { Button, Card, Dialog, forms } from "./components/index.js";
Imports are live, read-only bindings

This is the part that surprises people. An import is not a copy of the value — it is a live, read-only view of the export.

counter.js

JS
export let count = 0;
export function increment() { count++; }

app.js

JS
import { count, increment } from "./counter.js";

console.log(count);   // 0
increment();
console.log(count);   // 1  — the imported binding updated

// But the importer cannot mutate the binding directly:
// count = 5;          // TypeError: Assignment to constant variable.
0
1
Why this matters
If you re-assign an exported `let` from inside the module, every importer instantly sees the new value. This makes ES modules a natural place for shared state — and a footgun if you treat `import` like a snapshot.
Static, hoisted, and analysed before code runs

import and export are not normal statements. The engine collects them first, builds the module graph, then runs the code. Practical consequences:

  • import declarations are hoisted to the top of the module — you can use the binding in code that appears above the import line (though most teams put imports at the top anyway for readability).

  • You cannot put import { x } from path inside an if or a function. The path must be a string literal known at parse time.

  • Need conditional or runtime loading? Use the function-like import("...") instead — see Dynamic Imports.

  • Bundlers can statically discover unused exports and drop them — the famous tree shaking.

Cheat-sheet

Every shape, in one place

JS
// ---- exports ----
export const a = 1;
export function b() {}
export class C {}
export { a as one, b as two };       // rename
export default function () {}        // default
export * from "./other.js";          // re-export all named
export { x } from "./other.js";      // re-export one
export { default as Y } from "./y.js"; // re-export default as named

// ---- imports ----
import "./side-effect.js";            // run for side effects, import nothing
import a from "./mod.js";             // default
import { x, y } from "./mod.js";      // named
import { x as ex } from "./mod.js";   // rename
import a, { x } from "./mod.js";      // default + named
import * as m from "./mod.js";        // namespace
import a, * as m from "./mod.js";     // default + namespace

Read on for two close cousins: Dynamic Imports for loading on demand, and CommonJS vs ES Modules for the older Node-style require.