Load, link, evaluate
Learn how JavaScript loads a module graph, links live imports to exports, and evaluates each module in dependency order.
- 01Name the phasesExplain how loading, linking, and evaluation turn module files into a running graph.
- 02Trace live importsPredict when an imported binding changes because its exporting module changes it.
- 03Predict evaluationWalk a small dependency graph depth-first and post-order without running it first.
One graph, three phases
A module is not simply a file that runs when the browser sees it. Before any module body runs, the host builds a whole dependency graph. That preparation makes named imports reliable, makes circular imports possible to describe, and makes evaluation order predictable.
Module loading finds and parses every reachable module. Linking connects imports to exports. Evaluation runs module bodies after their dependencies are ready. These phases apply to one graph, not to one line at a time.
This lesson follows the browser security model. Browser rules decide whether a request is allowed; then the module system can load, link, and evaluate the allowed graph. The same broad idea appears in Node, although its resolver and loader have different host rules.
Keep one picture in mind. A module graph is a set of module records connected by import requests. The entry module is the starting point, but its dependencies often run first. That fact explains many surprising logs in real applications.
Source Text Module Records
A Source Text Module Record is the engine's record for one parsed JavaScript module. In plain words, it remembers which modules this source requests, which names it imports, which names it exports, and where the module is in its work: unlinked, linking, linked, evaluating, or evaluated.
export let count = 0; export function add() { count += 1;}Line 1 declares one exported binding named count. It starts at 0. Line 3 declares an exported function named add. The record can list these exports after parsing; it does not need to run add to discover the name.
import { count, add } from "./cart.js"; console.log(count);add();console.log(count);Line 1 requests ./cart.js and asks for its count and add exports. Lines 3 through 5 are evaluation work, so they wait until the graph has been prepared. The visible output is 0 and then 1.
The record is not a user-facing JavaScript object you normally inspect. It is useful vocabulary because it separates knowing a module's shape from running its statements. A syntax error can prevent a record from being created; a missing export can prevent linking.
Loading the module graph
Loading means finding, fetching, and parsing every module reachable from the entry. Starting with main.js, the host sees the request for ./cart.js, resolves it according to host rules, fetches it if needed, and parses it into another record.
import { count } from "./cart.js";console.log(count);Line 1 is enough to make cart.js part of this graph. Line 2 does not run while the loader is still only discovering files. It runs later, during evaluation, after linking connects count to a real export.
Before a group trip, you collect everyone's details. Then you make the WhatsApp group so people can contact the right person. Only after that does the trip start. Modules use the same helpful separation.
- In real life: Collect every friend's details
- In JavaScript: Load every reachable module
- In real life: Make one WhatsApp group
- In JavaScript: Link imports to exports
- In real life: Start the trip
- In JavaScript: Evaluate module bodies
- In real life: Friends prepare before the group leaves
- In JavaScript: Dependencies evaluate before importers
Where the analogy stops: Real module hosts also handle URLs, caching, errors, and security rules; the analogy only explains the phase order.
Loading is recursive because each fetched module can name more imports. A loader must avoid treating two requests for the same resolved module as two independent modules. The graph needs one record per module identity so shared dependencies can be linked and evaluated once.
Linking imports to exports
Linking connects an import request to an export binding. In main.js, the imported name count becomes a connection to the exported count binding in cart.js. It is not a separate number placed in a new variable.
import { count } from "./cart.js"; console.log(count);Line 1 asks for the export with the exact name count. Line 3 reads that binding. If cart.js did not export that name, the graph would fail while linking instead of quietly giving undefined.
This early failure is valuable. A module can be parsed successfully and still be unlinked because an import name is wrong. No body should be partly run merely because another part of the graph has a naming mistake.
| Phase | Main job | What has not happened yet |
|---|---|---|
| Load | Find, fetch, and parse every reachable module file. | The graph is discovered; imports are not connected yet. |
| Link | Connect each import name to the export it requests. | Bindings exist, but module bodies have not all run. |
| Evaluate | Run module bodies after their dependencies are ready. | Exports receive values and side effects occur in order. |
Live bindings, not copies
A live binding is an import that reads the current value of the exporter's binding. It behaves unlike copying a primitive into a new local variable. The exporting module owns its mutable binding; importing modules can read it but cannot assign to it.
import { count, add } from "./cart.js"; console.log(count);add();console.log(count);Line 3 prints 0 because cart.js created its binding with that value. Line 4 calls the exporting module's function. That function changes the same binding. Line 5 therefore prints 1, not a saved copy of 0.
Replay an instrumented lesson model of a live import. It explains module bindings; it is not an engine debugger.
script
console.log(cart.count);cart.add();console.log(cart.count);The replay is an instrumented lesson model. It runs the lesson's small cart function and shows the same state change that the module example describes. It does not pause or inspect the JavaScript engine.
Depth-first, post-order evaluation
Evaluation runs module bodies. For a normal synchronous graph, dependencies are evaluated before the module that imports them. A useful teaching model is depth-first, post-order: visit a dependency first, and add a module to the output only after its dependencies.
// 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");The graph says main.js imports a.js and b.js, while a.js imports shared.js. The logs are shared, a, b, and main. A dependency finishes before its importer logs.
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(", "));Line 9 remembers modules already seen. Line 12 visits every dependency before line 13 adds the current module. The final log on line 21 is shared.js, a.js, b.js, main.js. This is a teaching model, not a copy of an engine's source code.
Replay an instrumented depth-first, post-order teaching model. Real hosts also load and link before evaluation.
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
The graph becomes clearer when you change one edge. In the playground, remove only the import from a.js to shared.js. Then shared.js is no longer reachable from main.js, so it disappears from this modeled evaluation 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 edge changes which modules are reachable.
shared.js -> a.js -> b.js -> main.jsDependencies appear before their importers, and every reachable module appears once.
Because a.js imports shared.js, shared evaluates before a.js. main.js still waits for both a.js and b.js.
evaluationOrder function, not a browser module loader.Reset restores the original graph where a.js imports shared.js. The important rule is not that every file in a folder runs. Only modules reachable from the entry graph are loaded and eventually evaluated.
Module namespace objects
A module namespace object is the special value created by import * as cart from "./cart.js". It gives an object-like view of all exports. Its properties are live, but an importer cannot replace them with assignment.
// 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 creates the namespace binding named cart. Line 4 lists the export names, so this example prints add,count. Line 5 prints Module. Lines 6 through 8 show a live value: the count changes from 0 to 1.
In strict module code, cart.count = 2 throws a TypeError. That protects the exporter's binding. The exporting module can update its own let count; the namespace gives other modules a read-only view.
| Form | What it gives you | When it helps |
|---|---|---|
| Named import | A direct live binding such as count. | Use when the needed export is known. |
| Namespace object | A read-only object-like view of all exports. | Use import * as cart to inspect or group exports. |
| Plain object | An ordinary object whose properties you can replace. | Do not confuse it with a namespace object. |
How this helps in real apps
A feature module should import the specific shared values it needs. A checkout screen can import add and count from a cart module; it does not need to know whether that state came from a simple variable, a store, or a server-backed adapter.
import { add, count } from "./cart.js"; add();console.log(count);Line 1 names the two public exports. Line 3 updates cart state through its public function. Line 4 reads the current exported binding, so it prints 1 when this module starts with the lesson's cart state.
This phase model also helps debug failures. A network or URL problem is usually loading. An error about a missing export is linking. A thrown exception from a top-level statement is evaluation. Separating the words narrows the search quickly.
- Fetch
cart.jsaftermain.jsimports it. - Notice that
cart.jsexportscountandadd. - Connect imported
countto cart's exported binding. - Prepare the
cartnamespace view forimport * as cart. - Run
console.log("shared")inshared.js. - Run
add()so the exported count becomes 1.
Sort each event into the phase that owns it. Read the explanation after every card.
Common module mistakes
- “An import gets a copy.” Named imports are live bindings to the exporter's binding.
- “Loading runs code.” Loading discovers and parses the graph; evaluation runs bodies later.
- “Imports run top to bottom.” Dependencies run before importers, so graph order matters more than file order.
- “A namespace is a plain object.” It is a special read-only view with live export properties.
- “Each import reruns a shared module.” A module graph evaluates each reachable module once for that graph.
| Word | What it means | Do not confuse it with |
|---|---|---|
| Loading | Finding, fetching, and parsing the module graph. | Running module statements. |
| Linking | Resolving import names to export bindings. | Copying exported values into import variables. |
| Evaluation | Running a module after dependencies are ready. | Always running files in the textual import order. |
| Namespace object | A special read-only view of exports. | A mutable plain object made by the importer. |
These distinctions become especially important in the next lesson, where two modules can import each other. A cycle is not a reason to forget phases; it is a reason to be even more precise about what is linked before either body finishes evaluating.
Practice exercises
Read the tiny program. Type the two output lines in order.
let count = 0;
function add() { count += 1; }
console.log(count);
add();
console.log(count);The output is 0 then 1. The second read sees the changed binding.
A teammate asks which phase connects an import to an export. Give the phase name.
The phase is linking. It connects the requested import to the declared export.
Trace the teaching model and type its comma-separated output.
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 prevents duplicate evaluation.
What string does a module namespace object expose at Symbol.toStringTag?
A real module namespace object reports Module at that symbol.
In a module, predict what happens when code writes cart.count = 2 to a namespace export.
The assignment throws a TypeError. Update mutable exports inside the exporting module instead.
Your product page needs to add an item and show the cart count. What should it import from a shared cart module?
Import the named exports the feature needs, such as count and add. This creates a clear public connection through the module graph.
Check your understanding
Answer by naming the phase first, then trace a graph only when the question asks about order. For binding questions, ask which module owns the changing value and which module only reads it.
Question 1 of 7What is a Source Text Module Record in plain words?
Choose an answer to see the explanation.
Question 2 of 7What does the first live-binding model 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 phase connects
import { count }to the export namedcount?Choose an answer to see the explanation.
Question 4 of 7What does the evaluation model 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 7What is
cart[Symbol.toStringTag]for a real module namespace object?Choose an answer to see the explanation.
Question 6 of 7Can strict module code replace an export through
cart.count = 2?Choose an answer to see the explanation.
Question 7 of 7Why does the evaluation model keep a
visitedset?Choose an answer to see the explanation.
Key takeaways
- A Source Text Module Record remembers one parsed module's imports, exports, and phase.
- Loading discovers every reachable module; linking connects names; evaluation runs bodies.
- Named imports are live bindings, so importers observe updates made by the exporter.
- Normal synchronous evaluation is dependency-first: depth-first, post-order, and once per module.
- A namespace object is a read-only, live view of a module's exports with a
Moduletag. - Classify failures by phase before debugging URLs, exports, or top-level code.
Remember the one-liner.
A module graph is loaded, linked, and only then evaluated dependency first.
Coming next: Circular dependencies. You will use the same records, links, and evaluation states to explain what happens when two modules request each other.