JavaScript ES Modules (import and export) Guide

ES modules are JavaScript's standard way to split code into separate files and share functionality between them. They make code easier to organize, reuse, and maintain in both browser and Node.js environments.

Quick answer: Use export to make values available from one file and import to use them in another. ES modules use file-based dependencies, static syntax, and work with named or default exports.

Difficulty: Beginner

You'll understand this better if you know: basic variables, functions, and how JavaScript files run in the browser or Node.js.

1. What Are ES Modules?

ES modules, often called import/export modules, are the built-in module system defined by modern JavaScript. A module is just a file that can expose selected values and consume values from other files.

In a module, everything is private by default unless you export it. That is one of the biggest differences from older script-based code, where top-level declarations could be easier to leak into the global scope.

2. Why ES Modules Matter

ES modules solve one of the most common problems in JavaScript development: organizing growing codebases. As a project expands, you need a reliable way to share functions, constants, and classes without copying code everywhere.

They also help tools and runtimes understand your code better. Because module imports are static, bundlers and JavaScript engines can analyze dependencies before running the program. That makes optimization, tree shaking, and loading behavior more predictable.

Use ES modules when you want:

They are usually the right choice for new JavaScript projects unless you are working in an older environment that still requires CommonJS or another legacy system.

3. Basic Syntax or Core Idea

The core idea is simple: one file exports values, another file imports them. The syntax is built into the language and is always written at the top level of a module.

Named export

This example exports a function and a constant from one file.

export function greet(name) {
  return `Hello, ${name}!`;
}

export const appName = "DevDocs10";

Another file can import those values by name.

import { greet, appName } from "./utils.js";

console.log(greet("Ava"));
console.log(appName);

Named exports must be imported with the same names unless you rename them during import.

Default export

A file can also export one main value as the default export.

export default function formatPrice(value) {
  return `${value.toFixed(2)}`;
}

That default export can be imported without braces and can be given any local name.

import formatPrice from "./price.js";

console.log(formatPrice(19.5));

Think of named exports as multiple public tools from one file, while a default export is the file's primary value.

4. Step-by-Step Examples

Example 1: Sharing a helper function

Suppose you want to keep a formatting helper in a separate file. Export it once, then reuse it wherever needed.

// format.js
export function uppercaseName(name) {
  return name.trim().toUpperCase();
}

// app.js
import { uppercaseName } from "./format.js";

const displayName = uppercaseName("  ada  ");
console.log(displayName);

This pattern keeps data transformation logic in one place and avoids duplicate code.

Example 2: Renaming an import

You can rename a named export during import if the local name would conflict with another variable.

// math.js
export const pi = 3.14159;

// circle.js
import { pi as mathPi } from "./math.js";

const radius = 10;
const area = mathPi * radius * radius;

console.log(area);

This is useful when the exported name is descriptive but awkward in the local file.

Example 3: Exporting multiple values at once

You do not need to place export in front of every declaration. You can export several values together at the bottom of a file.

const formatDate = (date) => date.toISOString();
const formatUser = (user) => `${user.firstName} ${user.lastName}`;

export { formatDate, formatUser };

This style is convenient when you want to define several utilities first and export them together at the end.

Example 4: Importing everything as a namespace

Sometimes you want to keep module members grouped under one object-like name.

// string-tools.js
export function slugify(text) {
  return text.toLowerCase().replace(/\s+/g, "-");
}

export function trimLines(text) {
  return text.trim();
}

// page.js
import * as stringTools from "./string-tools.js";

console.log(stringTools.slugify("Hello World"));

Namespace imports are helpful when a module exposes several related utilities and you want the call sites to stay organized.

5. Practical Use Cases

ES modules are useful anywhere code needs to be separated into reusable pieces. Common real-world uses include:

Modules are especially helpful when a file begins to contain more than one responsibility. If a file is doing too many things, moving logic into smaller modules usually makes the code easier to test and reason about.

6. Common Mistakes

Mistake 1: Using the wrong braces for a default export

Beginners often treat default exports like named exports. That leads to a failed import because the syntax does not match the export type.

Problem: A default export is imported without braces. Using braces tells JavaScript to look for a named export with that name, which does not exist.

// logger.js
export default function logMessage(message) {
  console.log(message);
}

// app.js
import { logMessage } from "./logger.js";

Fix: Import the default export without braces.

// app.js
import logMessage from "./logger.js";

The corrected version works because the import syntax now matches the module's export type.

Mistake 2: Importing a name that was never exported

Named imports must match an actual named export. If they do not, the module fails to load.

Problem: This import asks for sum, but the module only exports add. In modern runtimes, this often produces an error such as The requested module does not provide an export named 'sum'.

// math.js
export function add(a, b) {
  return a + b;
}

// app.js
import { sum } from "./math.js";

Fix: Import the exact exported name, or rename it after import if you want a different local name.

// app.js
import { add as sum } from "./math.js";

This works because the module loader can now find the real export and map it to a local alias.

Mistake 3: Using module syntax in a non-module script

ES module syntax only works when the runtime treats the file as a module. If it does not, the parser stops before execution.

Problem: In the browser, a plain script tag without type="module" cannot use import or export. In Node.js, older setups may report Cannot use import statement outside a module.

// app.js
import { add } from "./math.js";

Fix: Make sure the file runs as a module. In the browser, load it with a module script tag.

<script type="module" src="./app.js"></script>

The fix works because the browser now parses the file using module rules instead of classic script rules.

7. Best Practices

Practice 1: Prefer named exports for most shared utilities

Named exports make it easier to discover what a module provides and reduce accidental renaming across files.

// preferred
export function parseDate(value) {
  return new Date(value);
}

export function formatDate(date) {
  return date.toLocaleDateString();
}

Use default exports only when the file truly has one main thing to share.

Practice 2: Keep module paths explicit and accurate

Use the correct relative path and file extension when the runtime requires it. Clear paths make modules easier to move and debug.

import { formatDate } from "./utils/date.js";

This avoids ambiguous imports and helps both browsers and Node.js resolve the file consistently.

Practice 3: Group related exports in one module

A module should usually represent one area of responsibility. Grouping related code keeps dependencies predictable.

// storage.js
export function saveUser(user) { /* ... */ }
export function loadUser(id) { /* ... */ }

When related functions stay together, the module boundary becomes meaningful instead of arbitrary.

8. Limitations and Edge Cases

These behaviors are often surprising when moving from older script patterns. The most common confusion is expecting imported values to behave like copied variables instead of connected bindings.

9. Practical Mini Project

Here is a tiny browser-based note formatter that uses one module for data helpers and another for the page logic. This example shows a realistic structure with named exports, a default export, and a module script.

// notes.js
export function createNote(text) {
  return {
    text,
    createdAt: new Date()
  };
}

export function formatNote(note) {
  return `[${note.createdAt.toLocaleTimeString()}] ${note.text}`;
}

export default function getNoteCountLabel(count) {
  return `${count} note${count === 1 ? "" : "s"}`;
}

// app.js
import getNoteCountLabel, { createNote, formatNote } from "./notes.js";

const notes = [];
const newNote = createNote("Write module docs");

notes.push(newNote);

console.log(formatNote(newNote));
console.log(getNoteCountLabel(notes.length));

This mini project shows the usual pattern: keep reusable logic in one module, then import only what the page needs. The default export provides a single main helper, while the named exports provide additional utilities.

10. Key Points

11. Practice Exercise

Create two files: one module that exports a string formatter and a number formatter, and one app file that imports both and logs the results.

Expected output: One cleaned username and one formatted dollar amount.

Hint: Use named exports for both functions, then import them with the same names inside braces.

// formatters.js
export function formatUsername(name) {
  return name.trim().toLowerCase();
}

export function formatCurrency(amount) {
  return `${amount.toFixed(2)}`;
}

// app.js
import { formatUsername, formatCurrency } from "./formatters.js";

console.log(formatUsername("  DEVdocs  "));
console.log(formatCurrency(12.5));

12. Final Summary

ES modules are the modern JavaScript way to split code across files and explicitly share values with export and import. They make dependencies easier to read, help avoid global-scope problems, and support predictable loading in browsers and Node.js.

The main syntax rules are simple once you learn them: named exports use braces, default exports do not, and module paths must point to the correct file. Most real-world mistakes come from mixing those patterns or trying to use module syntax in code that is not running as a module.

If you are building new JavaScript code, ES modules should usually be your default choice. A good next step is to learn how ES modules behave in the browser versus Node.js, especially around file paths, module scripts, and package configuration.