Load, link, evaluate
Learn how JavaScript loads a module graph, links live imports to exports, and evaluates each module in dependency order.
- 01Name the three stepsSay what loading, linking, and evaluation each do to a group of module files.
- 02Follow a live importPredict when an imported value changes because the file that exports it changes it.
- 03Predict the run orderWork out which file runs first in a small module graph, without running it.
Three steps: load, link, evaluate
A module is a JavaScript file that shares values with export and uses values from other files with import. The browser does not run a module the moment it sees it. First it gets every file ready. Only then does any code run.
The program that runs your JavaScript is called the host. It is the browser, or Node on a server.
Loading finds and reads every file the first file needs. Linking connects each import to the export it asks for. Evaluation runs the code in each file, imported files first. The three steps happen for the whole group of files, not one line at a time.
This lesson comes after the browser security model. Those browser rules decide whether a file may be downloaded at all. The steps in this lesson start after that. Node uses the same three steps, but it has its own rules for finding files.
Two more words you will see all through this lesson:
- The module graph is the map of which file imports which.
- The entry module is the file you start from, such as
main.js.
Loading starts at the entry module. But the files it imports usually run before it does. That explains many surprising logs in real apps.
The record for one module
When the engine reads a module file, it writes down notes about it. The spec, the official rule book for JavaScript, calls these notes a Source Text Module Record. In this lesson we just say module record. A module record keeps:
- which files this module imports from,
- which names it imports,
- which names it exports,
- which step it has reached:
unlinked,linking,linked,evaluating, orevaluated.
export let count = 0; export function add() { count += 1;}Line 1 exports a variable named count. It starts at 0. Line 3 exports a function named add. The engine can list both exports just by reading the file. It does not need to run add to learn its name.
import { count, add } from "./cart.js"; console.log(count);add();console.log(count);Line 1 asks ./cart.js for two of its exports: count and add. Lines 3 to 5 are ordinary code, so they wait until every file is ready. Then they print 0 and then 1.
Your code can't see a module record. It is just a useful name. It helps you keep two things apart: what a module contains, and running its code. For example:
- A typing mistake in the file, called a syntax error, stops the record from being made at all.
- Importing a name that the other file doesn't export stops linking.
Step 1: loading the files
Loading means finding, downloading, and reading every module the entry module needs. Reading a file to understand its structure is called parsing. Start from main.js. The host sees that it asks for ./cart.js. It works out the full address of that file, downloads it if needed, and parses it into another module record.
import { count } from "./cart.js";console.log(count);Line 1 is enough to add cart.js to the graph. Line 2 does not run while files are still being found. It runs later, in step 3, after linking has connected count to the real export.
Before a group trip, you collect everyone's phone number. Then you make one WhatsApp group, so everyone can reach the right person. Only then does the trip start. Modules work the same way: collect, connect, then go.
- In real life: Collect every friend's phone number
- In JavaScript: Load every module the entry needs
- In real life: Make one WhatsApp group
- In JavaScript: Link each import to its export
- In real life: Start the trip
- In JavaScript: Run the code in each module
- In real life: Friends get ready before the group leaves
- In JavaScript: Imported files run before the files that import them
Loading repeats itself. Each new file can import more files, and those get loaded too. If two files import the same file, the host loads it only once. Each module gets one record in the graph, so a shared file is linked once and run once.
Step 2: linking imports to exports
Linking connects each import to the export it asks for. In main.js, the imported name count gets connected to the exported count in cart.js. A name connected to a value like this is called a binding. Linking does not copy the number into a new variable.
import { count } from "./cart.js"; console.log(count);Line 1 asks for an export named exactly count. Line 3 reads it. If cart.js had no export called count, linking would fail with an error. You would not quietly get undefined.
This early error is useful. A file can be read without problems and still fail to link, because an import name is wrong. When linking fails, no code in the graph runs at all. You never get half a program.
Each row of this table is one step. Read across to see what the step does and what hasn't happened yet.
| Step | What it does | What hasn't happened yet |
|---|---|---|
| Load | Finds, downloads, and reads every module file the entry module needs. | The imports are not connected to exports yet. No module code has run. |
| Link | Connects each imported name to the export it asks for. | The names are connected, but no module code has run yet. |
| Evaluate | Runs the code in each module, imported files first. | Nothing. This is the last step: exports get their values and logs print. |
An import is not a copy
An imported name is a live binding. It always reads the value the exporting file has right now. That is different from copying a number into a new variable. The exporting file owns the variable. Files that import it can read it, but they can't give it a new value.
import { count, add } from "./cart.js"; console.log(count);add();console.log(count);Line 3 prints 0, because cart.js set count to 0. Line 4 calls add, a function inside cart.js. That function changes the same count. So line 5 prints 1, not an old copy of 0.
count in main.js points at the box in cart.js; it is not a copy. So after add() puts 1 in the box, line 5 reads 1.The step-through below uses a small stand-in for cart.js: a function called createCartModule that keeps count in a private variable. Reading cart.count calls a getter, a small function that hands back the current count. The stand-in shows the same change, one step at a time.
This replays a stand-in for a live import, one step at a time. It does not pause the real JavaScript engine.
script
console.log(cart.count);cart.add();console.log(cart.count);The step-through replays the stand-in code. It does not pause the real JavaScript engine.
Step 3: which file runs first
Evaluation runs the code in each module. A module runs only after the modules it imports have run. So imported files run before the files that import them.
// shared.jsconsole.log("shared"); // a.jsimport "./shared.js";console.log("a"); // b.jsconsole.log("b"); // main.jsimport "./a.js";import "./b.js";console.log("main");Here main.js imports a.js and b.js, and a.js imports shared.js. The logs come out as shared, a, b, and main. Each file finishes before the file that imports it logs anything.
shared.js runs first and main.js runs last.You can work out this order by hand with three rules:
- Start at the entry module.
- Before you write down a module, go into each of its imports, in order, and write those down first.
- Never write down the same module twice.
Programmers call this a depth-first, post-order walk. Depth-first means you follow one import all the way down before you start the next one. Post-order means a module is written down after its imports.
const graph = { "main.js": ["a.js", "b.js"], "a.js": ["shared.js"], "b.js": [], "shared.js": [],}; function evaluationOrder(graph, entry) { const visited = new Set(); const order = []; function visit(name) { if (visited.has(name)) return; visited.add(name); for (const dependency of graph[name] ?? []) visit(dependency); order.push(name); } visit(entry); return order;} console.log(evaluationOrder(graph, "main.js").join(", "));This program follows those rules. It uses a Set, a list that holds each value at most once.
- Line 9 makes the
visitedset, which remembers the modules already seen. - Line 12 stops if a module was seen before.
- Line 14 visits every import first.
- Only then does line 15 add the current module to
order. - Line 21 prints
shared.js, a.js, b.js, main.js.
This is a small model for learning, not the engine's real code.
This replays a small run-order program, one step at a time. A real host also loads and links every file before any of them run.
script
"main.js": ["a.js", "b.js"], "a.js": ["shared.js"], "b.js": [], "shared.js": [],}; function evaluationOrder(graph, entry) { const visited = new Set(); const order = []; function visit(name) { if (visited.has(name)) return; visited.add(name); for (const dependency of graph[name] ?? []) visit(dependency); order.push(name); } visit(entry); return order;} console.log(evaluationOrder(graph, "main.js").join(", "));Change one import
Change one import and watch the order change. In the playground, remove only the import from a.js to shared.js. Now nothing imports shared.js, so main.js can't reach it. It drops out of the run order.
const graph = { "main.js": ["a.js", "b.js"], "a.js": ["shared.js"], "b.js": [], "shared.js": [],}; function evaluationOrder(graph, entry) { const visited = new Set(); const order = []; function visit(name) { if (visited.has(name)) return; visited.add(name); for (const dependency of graph[name] ?? []) visit(dependency); order.push(name); } visit(entry); return order;} console.log(evaluationOrder(graph, "main.js").join(", "));shared.jsChanging this one import changes which modules main.js can reach.
shared.js -> a.js -> b.js -> main.jsImported files come before the files that import them. Each module that main.js can reach appears once.
a.js imports shared.js, so shared.js runs before a.js. main.js still waits for both a.js and b.js.
evaluationOrder program to work out the run order. It is not a real browser loading modules.Reset puts the import back. The rule to remember: not every file in a folder runs. Only modules that the entry module can reach through imports are loaded and run.
One object with every export
import * as cart from "./cart.js" gives you one object, cart, that holds every export of cart.js. This object is called a module namespace object. Its values are live, like any import. But the importing file can't give them new values.
// cart.js exports count and add.import * as cart from "./cart.js"; console.log(Object.keys(cart).join(","));console.log(cart[Symbol.toStringTag]);console.log(cart.count);cart.add();console.log(cart.count);- Line 2 makes the object named
cart. - Line 4 lists its property names with
Object.keys. It printsadd,count, because the names come out in alphabetical order. - Line 5 reads a special built-in key,
Symbol.toStringTag, which holds a short type name. Here it printsModule. - Lines 6 to 8 show the live value:
countgoes from0to1.
import * as cart gives you one object with every export in it. You can read cart.count, but only cart.js can change it.Module code always runs in strict mode, JavaScript's stricter set of rules. In strict mode, cart.count = 2 throws a TypeError, the error for using a value the wrong way. This protects the exporting file. cart.js can still change its own let count. Other files can only read it.
Each row of this table is a way to reach values. Read across to see what you get and when to use it.
| What you use | What you get | When to use it |
|---|---|---|
A named import, like import { count } | One live name, such as count. | Use it when you know which exports you need. |
A module object, from import * as cart | One read-only object that holds every export. Its values are live. | Use it to look at all the exports, or to keep them together under one name. |
A plain object, like { count: 0 } | An ordinary object. You can give its properties new values. | Use it for your own data. It is not a module object. |
How this helps in real apps
A feature file should import only the shared values it needs. A checkout page can import add and count from a cart module. It doesn't need to know how the cart keeps its data: in a simple variable, in a store, or on a server.
import { add, count } from "./cart.js"; add();console.log(count);Line 1 imports the two exports by name. Line 3 changes the cart through its own add function. Line 4 reads the current count. If the cart starts at 0, as in this lesson, it prints 1.
The three steps also help you find bugs. Ask which step failed:
- A wrong file address or a network problem happens during loading.
- An error about a missing export happens during linking.
- An error thrown by code at the top level of a file happens during evaluation.
Knowing the step tells you where to look.
- Download
cart.jsbecausemain.jsimports it. - Read
cart.jsand note that it exportscountandadd. - Connect the imported
countto thecountthatcart.jsexports. - Get the
cartmodule object ready forimport * as cart. - Run
console.log("shared")inshared.js. - Run
add()so the exportedcountbecomes 1.
Put each event in the step where it happens. Read the explanation after each card.
Common module mistakes
- “An import gets a copy.” An imported name is a live binding. It reads the exporting file's current value.
- “Loading runs code.” Loading only finds and reads the files. The code runs later, during evaluation.
- “Imports run top to bottom, like the lines in my file.” Imported files run before the files that import them. The module graph decides the order.
- “
import * asgives a plain object.” It gives a special module object. You can read its live values, but you can't assign to them. - “Each import runs a shared file again.” A file runs only once in a graph, no matter how many files import it.
Each row of this table is a word from this lesson. Read across to see what it means and what it does not mean.
| Word | What it means | What it does not mean |
|---|---|---|
| Loading | Finding, downloading, and reading every file in the module graph. | Running the code in those files. |
| Linking | Connecting each imported name to the export with the same name. | Copying exported values into new variables. |
| Evaluation | Running a module's code after the modules it imports have run. | Running files in the order the import lines are written. |
| Module namespace object | A special read-only object with every export of one module. | A plain object that the importing file can change. |
These differences matter even more in the next lesson. There, two modules import each other. That is called a cycle. To follow a cycle, you need to know exactly which names are linked before either file finishes running.
Practice exercises
Read the small program. Type the two lines it prints, in order.
let count = 0;
function add() { count += 1; }
console.log(count);
add();
console.log(count);It prints 0 and then 1. The second console.log reads the same count, after add() changed it.
A teammate asks: which step connects an import to an export? Type the name of the step.
The step is linking. It connects each import to the export with the same name.
Follow the small program by hand. Type what it prints, with the names separated by commas.
const graph = { main: ["a", "b"], a: ["shared"], b: [], shared: [] };
const visited = new Set();
const order = [];
function visit(name) {
if (visited.has(name)) return;
visited.add(name);
for (const dependency of graph[name]) visit(dependency);
order.push(name);
}
visit("main");
console.log(order.join(","));It prints shared,a,b,main. The visited set makes sure no module is added twice.
What text does a module namespace object hold at the key Symbol.toStringTag?
A real module namespace object holds the text Module at that key.
In a module, cart came from import * as cart. Then the code runs cart.count = 2. What happens?
The assignment throws a TypeError. To change count, call a function from cart.js, such as add().
Your product page needs to add an item and show how many items are in the cart. What should it import from a shared cart module?
Import the named exports the page needs: count and add. Then the page depends only on what the cart module chooses to share.
Check your understanding
For each question, first name the step: load, link, or evaluate. Trace the module graph only when a question asks about order. For a question about an imported value, ask two things: which file owns the value, and which file only reads it.
Question 1 of 7What is a module record, in plain words?
Choose an answer to see the explanation.
Question 2 of 7This small program acts like the live import. What does it print?
Read the code, then predictlet count = 0; function add() { count += 1; } console.log(count); add(); console.log(count);Choose an answer to see the explanation.
Question 3 of 7Which step connects
import { count }to the export namedcount?Choose an answer to see the explanation.
Question 4 of 7This program works out the run order. What does it print?
Read the code, then predictconst graph = { main: ["a", "b"], a: ["shared"], b: [], shared: [] }; const seen = new Set(); const order = []; function visit(name) { if (seen.has(name)) return; seen.add(name); for (const child of graph[name]) visit(child); order.push(name); } visit("main"); console.log(order.join(","));Choose an answer to see the explanation.
Question 5 of 7For a real module namespace object, what is
cart[Symbol.toStringTag]?Choose an answer to see the explanation.
Question 6 of 7In a module, can
cart.count = 2give an export a new value?Choose an answer to see the explanation.
Question 7 of 7Why does the lesson's run-order program keep a
visitedset?Choose an answer to see the explanation.
Key takeaways
- A module record is the engine's notes about one module file: its imports, its exports, and which step it has reached.
- Loading finds and reads every file the entry module needs. Linking connects each import to its export. Evaluation runs the code.
- An imported name is a live binding. When the exporting file changes the value, every file that imports it reads the new value.
- Imported files run before the files that import them. Each module runs only once.
import * asgives a module namespace object. You can read its live values, but you can't assign to them. Its tag isModule.- When something goes wrong, first ask which step failed: loading, linking, or evaluation.
In one line.
The host loads every module, links every import, and only then runs the code, imported files first.
Next lesson: circular dependencies, where two modules import each other. You will use the same module records, live bindings, and run order to see what happens.