⚡IntermediateModern JavaScript

Modules (import/export)

Organize code into reusable modules using ES6 import and export syntax named exports, default exports, re-exports, and dynamic imports.

Overview

ES6 modules (also called ESM ECMAScript Modules) are the official JavaScript standard for splitting code into separate files. Each file is its own module with its own scope variables and functions defined in a module are not globally visible unless explicitly exported. Other modules then import only what they need. Before ESM, JavaScript had no built-in module system. CommonJS (require/module.exports) was created for Node.js, and various bundlers provided their own solutions. ESM is now the standard: it works in all modern browsers with <script type="module">, in Node.js with .mjs files or "type": "module" in package.json, and is what TypeScript, Vite, and modern build tools use natively. Modules help you organize large codebases, avoid naming conflicts, make dependencies explicit, and enable tree-shaking (dead code elimination). Understanding named exports vs default exports, how to re-export, and how dynamic import() works for code splitting are all essential modern JavaScript skills.

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.14159

Default 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);  // info

Default 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.

Loading editor…

Completed: 0 / 5 exercises

Knowledge Check

Question 1 of 4

What is the difference between a named export and a default export?