JAVASCRIPT • FUNCTIONS AND PROGRAM STRUCTURE

ES Modules (export/import) — Export and import functions/values with ES Modules (export/import) (intro)

Learn how to split your JavaScript code into reusable, organized files using modern ES Module syntax.

Historical Context & Motivation

In the early days of web development, JavaScript was written entirely inside <script> tags placed directly in HTML files. Every variable and function you created lived in a single global scope, which meant that as projects grew larger, names would collide, bugs became harder to track, and code was nearly impossible to reuse across projects. Imagine trying to organize a school project where every team member dumps their notes into one giant, unsorted pile — that was the state of early JavaScript.

Developers needed a way to split code into separate, self-contained files — each responsible for a specific task — and then connect those files together cleanly. This idea is called modularity, and it is one of the most important principles in software engineering. Over time, several community-driven solutions emerged before the language itself adopted an official standard.

2009
CommonJS Introduced
The Node.js community created CommonJS modules using require() and module.exports, allowing server-side JavaScript to load files on demand.
2011
AMD & RequireJS
The Asynchronous Module Definition (AMD) pattern brought modular loading to the browser, letting scripts load without blocking the page.
2015
ES2015 (ES6) Specification
JavaScript officially added ES Modules to the language standard with export and import keywords — no libraries required.
2017–2018
Browser Support Arrives
Chrome, Firefox, Safari, and Edge shipped native support for <script type="module">, making ES Modules usable directly in browsers without a bundler.
2020+
Universal Standard
ES Modules are now the default module system in modern JavaScript, supported in both browsers and Node.js (v14+). Nearly all new projects use them.

The central question ES Modules answer is: how can we write JavaScript in separate files, share only the parts we choose, and keep everything else private? Understanding the answer unlocks the ability to build larger, cleaner, more maintainable programs.

Core Principles & Definitions

Before diving into code, let's establish the foundational ideas behind ES Modules. Each principle below builds on the previous one, so read them in order.

1

Module = File

In ES Module syntax, every .js file is treated as its own module. Variables and functions inside a file are private by default — nothing leaks out unless you explicitly share it.
2

export — Share Outward

The export keyword marks a function, variable, or class as available to other files. Think of it as putting an item on a shelf that others can take.
3

import — Bring In

The import keyword lets another file grab specific exports. You choose exactly what you need — no more, no less.
4

Named vs. Default

A module can have many named exports (each with a specific name) and at most one default export (the main thing the file provides).
5

Static Structure

ES Module imports and exports are analyzed before the code runs. This means the JavaScript engine knows the entire dependency graph at load time, enabling optimizations like tree-shaking (removing unused code).
✦ KEY TAKEAWAY
Think of each JavaScript file as a locked room. By default, nothing inside can be seen from the outside. The export keyword is like putting specific items on display in a window, and import is like reaching through that window to grab exactly what you need. You can never accidentally take something that wasn't displayed — that's what makes modules safe and predictable.

Visual Explanation — How Modules Connect

The diagram below shows a typical project with three files. The math.js module exports two named functions. The greeting.js module exports one default function. The app.js file imports from both modules, using only what it needs.

The dashed arrows represent the flow of exports: math.js shares two named functions, while greeting.js shares one default function. The app.js entry point uses import to pull in only the pieces it needs.

Notice that app.js uses curly braces { add, multiply } for named imports but no curly braces for the default import greet. This syntax difference is one of the most common sources of confusion for beginners, so keep an eye on it as we explore further.

How ES Modules Work Under the Hood

When a browser or Node.js encounters an ES Module, it goes through three distinct phases before your code actually runs. Understanding these phases helps explain why certain rules exist — like why you cannot put import statements inside if blocks.

Phase 1 — Parsing & Construction

The engine reads through all of your files and builds a module graph — a map that shows which files depend on which other files. It finds every import statement and fetches the corresponding file. Because imports must appear at the top level (not inside functions or conditionals), the engine can build this map before running a single line of your code.

Phase 2 — Instantiation

The engine allocates memory for every exported binding. Think of this step as labeling empty shelves: the engine knows the names of everything that will be exported, but the values haven't been assigned yet. Importantly, imports and exports are live bindings — the importing file doesn't get a copy of the value; it gets a live reference to the original variable in the exporting module.

Phase 3 — Evaluation

Finally, the engine executes each module's code in depth-first order, starting from modules with no dependencies (leaf nodes) and working back to the entry point. Each module is evaluated only once, even if multiple files import it. The values fill the shelves created in Phase 2.

The three-phase loading process ensures the JavaScript engine understands the full dependency structure before any code executes. This static analysis is what makes tree-shaking and other build-time optimizations possible.

Named Exports vs. Default Exports — A Detailed Breakdown

ES Modules give you two flavors of exporting: named exports and default exports. You can even mix both in the same file. The table below compares them side by side so you can see when to use each approach.

Named vs. Default exports comparison
FeatureNamed ExportDefault Export
Syntax (exporting)export function add() { } or export { add, multiply };export default function greet() { }
Syntax (importing)import { add } from './math.js';import greet from './greeting.js';
How many per file?Unlimited — as many as you needAt most one per file
Curly braces?Required: { name }Not used — any name works
Renaming on importimport { add as sum } from '...'You choose the name freely: import sayHello from '...'
Best forUtility libraries with many exports (e.g., math helpers)Files with one primary purpose (e.g., a component)

Inline Export vs. Export List

Named exports can be written in two equivalent ways. You can place the export keyword directly in front of each declaration (inline style), or you can declare everything normally and then add a single export list at the bottom of the file. Both produce the same result, so choose whichever style your team prefers for readability.

⚠️ Common Mistake Alert
When importing a named export, the name inside the curly braces must match exactly. Writing import { Add } from './math.js' will fail if the module exports add (lowercase). JavaScript is case-sensitive! Use the as keyword if you need a different name: import { add as Add } from './math.js'.

Worked Example — Building a Mini Utility Library

Let's walk through creating a small project that uses both named and default exports. We'll build a temperature converter library and an entry-point file that uses it.

Temperature Converter with ES Modules
1
Step 1 — Create the converter module (converter.js)We create a file called converter.js and define two named exports: one function to convert Fahrenheit to Celsius and another to convert Celsius to Fahrenheit. // converter.js export function toCelsius(f) { return (f - 32) * 5 / 9; } export function toFahrenheit(c) { return c * 9 / 5 + 32; }
Two named exports: toCelsius and toFahrenheit
2
Step 2 — Create a greeting module (greeting.js)We create a second module with a single purpose — generating a formatted greeting message. Since the file has one main job, we use a default export. // greeting.js export default function greet(city) { return `Welcome to the weather report for ${city}!`; }
One default export: greet
3
Step 3 — Import everything in the entry point (app.js)In app.js, we import the named exports using curly braces and the default export without braces. Notice the file paths start with ./ to indicate a local file. // app.js import { toCelsius, toFahrenheit } from './converter.js'; import greet from './greeting.js';
Named imports use { }; default imports do not.
4
Step 4 — Use the imported functionsWe call the imported functions just like any other function. Everything is scoped — no global pollution. console.log(greet('Denver')); // → "Welcome to the weather report for Denver!" console.log(toCelsius(98.6)); // → 37 console.log(toFahrenheit(0)); // → 32
Output: "Welcome to the weather report for Denver!", 37, 32
5
Step 5 — Run as a module in the browserTo use ES Modules in an HTML file, add the type="module" attribute to your script tag. Without this attribute, the browser treats the file as a regular script and will throw errors on import statements. <script type="module" src="./app.js"></script>
The type="module" attribute is required for ES Modules to work in browsers.

Strengths & Limitations of ES Modules

ES Modules are powerful, but like any tool they come with trade-offs. The table below summarizes the key advantages and limitations you should be aware of when using them in real projects.

Strengths and limitations of ES Modules
Strengths ✅Limitations ⚠️
Official language standard — works everywhere without extra librariesOlder browsers (IE 11 and below) do not support ES Modules
Static analysis enables tree-shaking, reducing bundle sizeCannot conditionally import (no import inside if blocks — must use dynamic import() for that)
Encapsulation — variables stay private unless exportedLocal file paths must include the file extension (.js) in browsers
Live bindings keep values in sync across filesModules always run in strict mode — no with statements, no silent errors
Clear dependency graph makes debugging easierCircular dependencies (A imports B which imports A) can cause confusion
✦ KEY TAKEAWAY
Think of ES Modules like the chapters in a well-organized textbook. Each chapter covers a specific topic (encapsulation), the table of contents tells you where everything is (static structure), and you can flip to any chapter without reading the others (selective imports). The slight downside? You need a textbook that supports the table-of-contents format — old browsers are like outdated editions that don't.

Connection to Advanced Module Patterns

Now that you understand static import and export, it's worth knowing that JavaScript also supports more advanced module patterns. These build on the same ES Module system but add flexibility for complex situations.

Introductory vs. advanced module concepts
ConceptWhat You Learned (Intro)Advanced Version
Static Importimport { add } from './math.js'Dynamic Import: const mod = await import('./math.js') — loads modules on demand at runtime
Individual Importsimport { add, multiply } from ...Namespace Import: import * as math from './math.js' — bundles all exports into one object
Direct ExportEach file exports its own codeRe-export: export { add } from './math.js' — a file can forward exports from another module
Single EntryOne app.js file imports everythingBarrel Files: An index.js re-exports from many modules, creating a clean public API

You don't need to master these advanced patterns right now, but knowing they exist gives you a roadmap. As your projects grow — especially if you use frameworks like React or Vue — you'll encounter dynamic imports for code-splitting, barrel files for clean library APIs, and namespace imports for large utility modules. The foundation you've built today makes learning those patterns much easier.

Practice Problems

PROBLEM 1 — CONCEPTUAL
Explain in your own words: what is the difference between a named export and a default export? When would you choose one over the other?
PROBLEM 2 — BASIC CALCULATION
Given the following module, write the correct import statement to use square in another file: // mathUtils.js export function square(n) { return n * n; } export function cube(n) { return n * n * n; }
PROBLEM 3 — INTERMEDIATE
A student writes the following code and gets an error. Identify the bug and fix it. // logger.js export default function log(msg) { console.log(`[LOG]: ${msg}`); } // app.js import { log } from './logger.js'; log('Hello!');
PROBLEM 4 — APPLIED
You're building a simple web app with three files: validators.js (provides isEmail and isNotEmpty functions), formHandler.js (a default export of a function that processes form data), and main.js (the entry point). Write all three files with proper export/import statements. The formHandler should use the validators internally.
PROBLEM 5 — CRITICAL THINKING
Consider this scenario: You have 50 utility functions in a file called utils.js, but app.js only needs 3 of them. A teammate suggests using import * as utils from './utils.js' for convenience. Evaluate this approach: what are the advantages and disadvantages compared to importing only the 3 functions by name? How does this relate to tree-shaking?

Lesson Summary

ES Modules are JavaScript's official system for splitting code into reusable files. Each file is a module where variables and functions are private by default. You use the export keyword to make specific values available to other files and the import keyword to bring those values in. There are two flavors: named exports (imported with curly braces, unlimited per file) and default exports (imported without braces, one per file).

The module system works through three loading phases: parsing (building the dependency graph), instantiation (creating live bindings), and evaluation (running the code). This static structure enables powerful optimizations like tree-shaking. To use modules in the browser, add type="module" to your script tag. Mastering ES Modules is a foundational skill for writing organized, maintainable JavaScript in any modern project.

Varsity Tutors • JavaScript • ES Modules (export/import)