JavaScript - Modules (ESM)

Overview

Estimated time: 35–45 minutes

ES Modules (ESM) provide a standard way to organize code across files with explicit imports/exports. Learn named vs default exports, namespace imports, re-exports, dynamic import(), and key differences in browsers vs Node.js.

Learning Objectives

  • Export and import values using named and default exports.
  • Use namespace imports and re-export from other modules.
  • Load code dynamically with import() and understand module scope.
  • Be aware of browser vs Node.js ESM differences and common pitfalls.

Prerequisites

Exports and imports

// math.js (module)
export const add = (a, b) => a + b;
export function mul(a, b) { return a * b; }
const PI = 3.14159;
export default PI; // default export
// app.js (module)
import PI, { add, mul } from './math.js';
console.log(add(2, 3));  // 5
console.log(mul(2, 3));  // 6
console.log(PI);         // 3.14159

Namespace import and re-exports

// Namespace import
import * as math from './math.js';
math.add(1,2); math.mul(2,3); math.default; // default via .default

// Re-export
export { add, mul } from './math.js';     // re-export named
export { default as PI } from './math.js'; // re-export default under a name

Dynamic import()

import() loads modules at runtime and returns a promise, enabling conditional and on-demand loading.

async function loadCalc() {
  const mod = await import('./math.js');
  console.log(mod.add(1,2));
}

Module scope and top-level await

  • Each module has its own scope; variables are not global unless explicitly attached.
  • Modules are singletons per URL: importing the same URL returns the same module instance.
  • Top-level await is allowed in ESM; dependent modules wait for it to resolve.

Browser vs Node.js notes

  • Browsers require valid module MIME type and often explicit file extensions in import paths.
  • Node.js enables ESM via "type": "module" in package.json or using .mjs files. Resolution differs from browsers.
  • CommonJS interop: default import from a CJS module may yield the exports object; prefer import pkg from 'cjs' then use pkg.default || pkg as needed.
  • Import maps can help resolve bare specifiers in browsers.

Common Pitfalls

  • Extension and path: In browsers, use ./ and include .js (e.g., import './utils.js').
  • Mixed systems: Avoid mixing CommonJS require() with ESM import in the same file.
  • CORS/MIME: Cross-origin modules require CORS; servers must serve JS as text/javascript.

Try it

Run to create a module on-the-fly via Blob and import it dynamically: