Modules — import and export

Covers export, which makes values in a file public, and import, which loads them. Learn named vs. default exports, type="module" in the browser, and ESM vs. CommonJS in Node.js.

If you keep adding the code for an order screen to a single file, tax calculations, the cart, and checkout logic end up mixed together over hundreds of lines, and finding a function takes a while. Even if you split the code into files and list several <script> tags (the tag that loads a JavaScript file) in the HTML, the code that uses a function can't tell which file it lives in.

This article covers export and import, which pass values between files.

You Can't Use import in the Console

The exercise console runs your code by placing it inside a single function. import and export can only be written at the top level of a file, so they cause a SyntaxError there. The code in this article is meant to run in Node.js or the browser as separate files, and the results are shown in comments at the end of each line.

Sharing Functions Between Files — Named Exports

Suppose both cart-page.js (the cart screen) and checkout.js (the checkout screen) need the same tax calculation. If you copy the function into both files, a tax rate change means fixing two places, and if you miss one, the screens show different amounts.

Variables and functions declared in a module (a JavaScript file that passes values with import and export; how to load one is covered in a later section) aren't visible to other files. Putting export in front of a declaration makes it public; this is called a named export. The file that uses it writes the same name inside braces: import { withTax } from "./price.js";.

// ---- price.js ----
const TAX_RATE = 0.1;                              // No export: used only inside price.js

export function withTax(price) {                   // With export: other files can import it
  return Math.floor(price * (1 + TAX_RATE));
}
export function formatYen(price) {
  return `${price} yen`;
}

// ---- cart-page.js ----
import { withTax, formatYen } from "./price.js";   // List the names you'll use inside the braces
console.log(formatYen(withTax(4800)));             // 5280 yen

// If you write a name that isn't exported in the import
// import { TAX_RATE } from "./price.js";
// SyntaxError: The requested module './price.js' does not provide an export named 'TAX_RATE'
Names You Can and Can't Import
import { withTax,formatYen }price.jsexports themBoth functionsare loadedPrints5280 yenimport{ TAX_RATE }TAX_RATE isnot exportedFails witha SyntaxErrorNo line ofcart-page.js runs
The two names in the top row are exported, but TAX_RATE in the bottom row isn't. You can only import names that are exported.

TAX_RATE is read by withTax inside price.js, so cart-page.js gets the price with tax without knowing the tax rate. If the rate changes, you only fix the one TAX_RATE line in price.js, and every file that imports it picks up the change.

Making a Single Class Public — Default Exports

If cart.js holds the cart class, the main thing it exports is a single Cart. You'll often see import Cart from "./cart.js"; in other people's code, but adding braces as in the previous section gives a SyntaxError.

A default export (at most one per file; the importing file doesn't need to match its name) is written as export default class Cart { ... }. The importing file writes it without braces and can use any name it likes in place of Cart.

// ---- cart.js ----
import { withTax } from "./price.js";
export const MAX_ITEMS = 20;                       // Named exports can live in the same file
export default class Cart {                        // One default export per file
  #prices = [];
  add(price) { this.#prices.push(price); }
  get total() { return withTax(this.#prices.reduce((sum, price) => sum + price, 0)); }
}

// ---- checkout.js ----
import Cart, { MAX_ITEMS } from "./cart.js";       // Default outside the braces, named inside
import ShoppingCart from "./cart.js";              // A default can be imported under another name
const cart = new ShoppingCart();
cart.add(1200); cart.add(3600);
console.log(cart.total, MAX_ITEMS);                // 5280 20
console.log(Cart === ShoppingCart);                // true (both are the same class)

// With braces, it looks for a named export called Cart
// import { Cart } from "./cart.js";
// SyntaxError: The requested module './cart.js' does not provide an export named 'Cart'
Braces Decide Which Export Is Looked Up
import Cartfrom "./cart.js"No braces:looks for defaultThere's an exportdefault class CartYou can usenew Cart()import { Cart }from "./cart.js"Braces: looks fora named exportNo named exportcalled CartFails witha SyntaxError
The bottom row looks for a named export called Cart and fails. An import without braces gets the default export, not a named one.

The 'Cart' at the end of the error message is the name of the named export it looked for and couldn't find. The message doesn't mention that a default Cart exists, so check whether you used braces. The table below sums up the import forms used in this article.

import formWhat you getName on the importing side
import { withTax } from …Named exportSame name as the export
import Cart from …Default exportAny name you like
import Cart, { MAX_ITEMS } from …Default and namedCart and MAX_ITEMS (default first)

Letting the Browser Follow Imports — type="module"

Next, the checkout page's HTML needs to load checkout.js. With a plain <script src="./checkout.js"></script>, Chrome reports SyntaxError: Cannot use import statement outside a module at the import on line 1, and none of the file runs.

With type="module" (a <script> setting that runs the loaded file as a module), the browser follows the imports in the entry file and fetches the other files too. These import connections are called the dependency graph (which files import which).

// ---- index.html: list only the entry file, checkout.js ----
// <script type="module" src="./checkout.js"></script>

// ---- price.js ----
console.log("Running price.js");                   // Printed once, even though 2 files import it
const TAX_RATE = 0.1;
export function withTax(price) { return Math.floor(price * (1 + TAX_RATE)); }

// ---- cart.js ----
import { withTax } from "./price.js";
console.log("Running cart.js");
export const cartTotal = withTax(4800);

// ---- checkout.js ----
import { cartTotal } from "./cart.js";
import { withTax } from "./price.js";              // Loads the same price.js as cart.js
console.log(`Total due: ${cartTotal + withTax(500)} yen`);

// Console output order: Running price.js → Running cart.js → Total due: 5830 yen
The Dependency Graph Starting from checkout.js
index.htmltype="module"checkout.js③ runs lastcart.js② runs nextprice.js① runs first, once
Except for the one from index.html, the arrows show the direction of each import. Execution starts with price.js, and it runs only once even though two arrows point to it.

A module runs the files it imports before it runs itself. The entry file, checkout.js, waits until the cart.js and price.js it imports have finished, so it runs last. Since the order is decided by the imports, you don't need to think about the order of files in the HTML.

It Doesn't Work in HTML Opened via file://

If you double-click an HTML file and open it as file://, Chrome treats even files in the same folder as coming from a different origin (the source a file is fetched from), and CORS (the mechanism that restricts loading from other origins) blocks the type="module" load. Start a web server, for example with the Live Server extension for VS Code, and open the page from there.

Telling the Formats Apart in Node.js — ESM and CommonJS

In code written for Node.js, you'll come across files that use const { reserve } = require("./stock"); instead of import. If you copy a line like that into a file that uses import, it throws a ReferenceError at runtime.

Besides ESM (short for ES Modules, the standard format that uses import and export), there's CommonJS (a Node.js-specific format that loads with require and makes values public with module.exports). Node.js decides which one a file uses based on its extension and the "type" field in package.json (the project's configuration file).

// ---- shop/package.json (the file with a Node.js project's settings) ----
// { "type": "module" }

// ---- shop/stock.cjs: its extension is .cjs, so it's loaded as CommonJS ----
function reserve(count) { return `Reserved: ${count}`; }
module.exports = { reserve: reserve, LIMIT: 3 };   // Put all the public values in one object

// ---- shop/report.cjs: a CommonJS file loads with require ----
const { reserve, LIMIT } = require("./stock.cjs");
console.log(reserve(2), LIMIT);                    // Reserved: 2 3

// ---- shop/checkout.js: "type": "module", so it's loaded as ESM ----
import stock from "./stock.cjs";                   // No braces: gets the value of module.exports
console.log(stock.reserve(1));                     // Reserved: 1
const { LIMIT } = require("./stock.cjs");
// ReferenceError: require is not defined in ES module scope, you can use import instead
Files Affected by "type": "module"
shop folder — "type": "module" in package.json
  • Files ending in .js are loaded as ESM
checkout.js — ESM
  • Can load it with import stock from "./stock.cjs"
  • Writing require throws a ReferenceError
.cjs extension — CommonJS
  • stock.cjs — makes values public with module.exports
  • report.cjs — can load with require
"type": "module" makes the .js files in the folder ESM. A .cjs file stays CommonJS, even in the same folder.

import stock has no braces, so it works the same way as importing a default export: it gets the module.exports object. Don't mix import and require in one file; stick to one or the other. The table below shows how to write each format and the rules that decide a file's format.

ItemESMCommonJS
Loadingimport { reserve } from "./stock.js"const { reserve } = require("./stock.js")
Making publicexport function reserve() { ... }module.exports = { reserve: reserve }
ExtensionCan't be omitted in Node.js or browsers ("./stock" isn't found)Can be omitted ("./stock" finds stock.js)
Files loaded in this format.mjs, and .js under "type": "module".cjs, and .js under "commonjs" or no "type" (with no "type", a .js that uses import is re-read as ESM)
BrowserLoads with type="module"Not supported directly
QUIZ

Knowledge Check

Answer each question one by one.

Q1What happens if you load the default export Cart with import { Cart } from "./cart.js"?

Q2price.js is imported from two places. How many times does the console.log at its top print?

Q3What happens if you call require in checkout.js under "type": "module"?