ES Modules (import/export)
Learn how to split JavaScript code across multiple files using ES modules — named exports, default exports, import syntax, and how ES modules compare to CommonJS.
Reading Time
16 min
Lesson
Lesson 28 of 48
Why Modules?
As programs grow, keeping everything in one file becomes unmanageable — variable names collide, related code gets scattered, and reasoning about what depends on what becomes difficult. Modules let you split code across multiple files, each with its own private scope, and explicitly declare what it shares with the outside world using export, and what it needs from other files using import.
Named Exports
A named export lets a module share zero, one, or many values under specific names. You can export a value directly at its declaration with the export keyword, or declare everything first and export a group of names together in a single export statement at the bottom of the file. Named exports must be imported using the exact same name they were exported with (unless you rename them, shown shortly).
math.js — Named Exports
Console Output
Click “Run” to see the console output here.
Importing Named Exports
Console Output
Click “Run” to see the console output here.
Default Exports
A module can also have one default export — the single "main" thing that module is considered to provide. Unlike named exports, a default export is imported without curly braces, and the importing file can call it whatever name it wants, since there's no exported name to match. A file can mix a default export with named exports, though it's common for small, focused modules to use only a default export.
user.js — Default Export
Console Output
Click “Run” to see the console output here.
Importing a Default Export
Console Output
Click “Run” to see the console output here.
Renaming Imports and Namespace Imports
If an imported name would clash with something already in scope, use the as keyword to rename it during import. If you want access to every named export from a module without listing each one, import * as someName gathers them all into a single object, with each export available as a property on it.
Renaming and Namespace Imports
Console Output
Click “Run” to see the console output here.
Modules Are Automatically Strict and File-Scoped
Every ES module runs in strict mode automatically — you never need to write "use strict" yourself. Modules also get their own top-level scope: a variable declared at the top of one module is NOT visible in another module unless it's explicitly exported and imported. This is a big improvement over plain scripts, where every top-level variable used to pollute one shared global scope.
Static Import vs. Dynamic import()
The import statement you've seen so far is static — it's hoisted, resolved at load time, and must appear at the top level of a file, not inside an if statement or function. Dynamic import(), by contrast, is a function-like expression you can call anywhere in your code. It returns a Promise that resolves to the module, making it useful for loading code lazily — for instance, only when a certain feature is actually used.
Dynamic import()
Console Output
Click “Run” to see the console output here.
ES Modules vs. CommonJS
| Feature | ES Modules | CommonJS |
|---|---|---|
| Export syntax | export / export default | module.exports / exports.name |
| Import syntax | import ... from "..." | require("...") |
| Loading | Static, resolved at parse time (plus dynamic import()) | Dynamic, resolved at runtime |
| Strict mode | Automatic | Not automatic |
| Where used | Modern browsers and modern Node.js | Older and current Node.js codebases |
You Will See Both in the Wild
Plenty of existing Node.js code, libraries, and tutorials still use CommonJS's require() and module.exports rather than import/export. The two systems solve the same problem — sharing code between files — with different syntax and different loading behavior (CommonJS is synchronous and dynamic, ES modules are primarily static). Modern JavaScript and modern Node.js favor ES modules, but recognizing CommonJS is still an essential, practical skill.
One Default Export per Module
A module can only have a single default export, but as many named exports as you like. A common convention is to default-export the one primary thing a file provides (a component, a class, a main function) and use named exports for smaller, secondary helpers that file also wants to share.
Import Paths and File Extensions
Relative import paths must start with ./ or ../ — a bare name like "math" is reserved for packages resolved from node_modules, not your own files. Depending on your build tooling, you may also need to include the file extension (like ./math.js) since not every environment resolves extensions automatically the way bundlers often do.