Modules (import/export)
Organize code into reusable modules using ES6 import and export syntax named exports, default exports, re-exports, and dynamic imports.
Overview
Key Points
- •export keyword makes values available to other modules; import brings them in
- •Named exports: export const foo = 1; imported as import { foo } from "./file"
- •Default export: export default function() {} imported as import anything from "./file"
- •A module can have many named exports but only one default export
- •import { foo as bar } from "./file" renames foo to bar in the importing scope
- •import * as ns from "./file" imports all named exports as properties of ns
- •Re-export: export { foo } from "./other" passes through another module's exports
- •Dynamic import: import("./file").then(module => ...) loads modules on demand
Examples
Named exports and imports
// math.js
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
// Not exported private to this module
function _helper() {}
// ----------------------------------------
// app.js
import { PI, add, multiply } from "./math.js";
console.log(PI); // 3.14159
console.log(add(2, 3)); // 5
console.log(multiply(4, 5)); // 20
// Import with alias
import { add as sum } from "./math.js";
console.log(sum(1, 2)); // 3
// Import everything
import * as math from "./math.js";
console.log(math.PI); // 3.14159Default export and mixing with named exports
// logger.js
export default class Logger {
constructor(prefix) {
this.prefix = prefix;
}
log(message) {
console.log(`[${this.prefix}] ${message}`);
}
error(message) {
console.error(`[${this.prefix}] ERROR: ${message}`);
}
}
// Named export alongside default
export const LOG_LEVELS = { INFO: "info", WARN: "warn", ERROR: "error" };
// ----------------------------------------
// app.js
import Logger, { LOG_LEVELS } from "./logger.js"; // default + named
const logger = new Logger("App");
logger.log("Started"); // [App] Started
logger.error("Something bad"); // [App] ERROR: Something bad
console.log(LOG_LEVELS.INFO); // infoDefault exports can be imported with any name you choose. Named exports must use the exact exported name (or an alias).
Dynamic import for code splitting
// Static imports load at startup (top of file)
// Dynamic import loads on demand returns a Promise
async function loadChartLibrary() {
// Loads only when needed, not at startup
const { default: Chart, drawBar } = await import("./chart.js");
return { Chart, drawBar };
}
// Common pattern: load heavy features on user action
document.querySelector("#showChart")?.addEventListener("click", async () => {
const { Chart } = await loadChartLibrary();
const chart = new Chart("#canvas");
chart.render([1, 2, 3, 4, 5]);
});
// Works in Node.js too useful for optional peer dependencies
async function tryLoadOptional() {
try {
const mod = await import("optional-package");
return mod.default;
} catch {
return null; // package not installed degrade gracefully
}
}Dynamic import() is essential for code splitting in large apps only load the code users actually need.
Common Mistakes
- Confusing default and named imports default is import Foo from ..., named is import { foo } from ...
- Using require() syntax in an ES module context .mjs files and type:module projects use import/export only
- Importing from a module that has no such export you get undefined, not an error, making bugs subtle
- Circular imports module A imports from B which imports from A can lead to undefined values at initialization
Note
Prefer named exports over default exports for most things in a project. Named exports are more explicit, easier to refactor (your IDE knows the name), and tree-shaking works better with them. Reserve default exports for the single "main thing" a module provides.
Exercises
Write JavaScript code to solve each exercise. Run it and check your output.
Completed: 0 / 5 exercises
Knowledge Check
Question 1 of 4