cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Load, link, evaluate

Learn how JavaScript loads a module graph, links live imports to exports, and evaluates each module in dependency order.

By the end, you can
  • 01
    Name the phasesExplain how loading, linking, and evaluation turn module files into a running graph.
  • 02
    Trace live importsPredict when an imported binding changes because its exporting module changes it.
  • 03
    Predict 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.

Definition

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.

A tiny entry modulePop out in the code editor (opens in a new tab)JavaScript
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.

Real-life analogyPlanning a group trip

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.

A named import points at an exportPop out in the code editor (opens in a new tab)JavaScript
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.

Keep the three phases separate
PhaseMain jobWhat has not happened yet
LoadFind, fetch, and parse every reachable module file.The graph is discovered; imports are not connected yet.
LinkConnect each import name to the export it requests.Bindings exist, but module bodies have not all run.
EvaluateRun 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.

main.js reads one changing exportPop out in the code editor (opens in a new tab)JavaScript
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.

Step through one live import
Step 0 of 5Ready
Your turn: follow the blue line

Replay an instrumented lesson model of a live import. It explains module bindings; it is not an engine debugger.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
 console.log(cart.count);cart.add();console.log(cart.count);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

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.

Files that log when they evaluateJavaScript
// 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.

A teaching model for evaluation orderPop out in the code editor (opens in a new tab)JavaScript
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.

Step through dependency-first evaluation
Step 0 of 6Ready
Your turn: follow the blue line

Replay an instrumented depth-first, post-order teaching model. Real hosts also load and link before evaluation.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  "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(", "));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

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.

Playground: change one import
Evaluation-order modelPop out in the code editor (opens in a new tab)JavaScript
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(", "));
Modeled console output4 modules
a.js importsshared.js

Changing this one edge changes which modules are reachable.

evaluation ordershared.js -> a.js -> b.js -> main.js

Dependencies appear before their importers, and every reachable module appears once.

Try it yourself

Because a.js imports shared.js, shared evaluates before a.js. main.js still waits for both a.js and b.js.

This is a teaching model of depth-first, post-order evaluation. It uses the lesson's 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.

main.js gets a namespace viewPop out in the code editor (opens in a new tab)JavaScript
// 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.

Three ways values can look object-like
FormWhat it gives youWhen it helps
Named importA direct live binding such as count.Use when the needed export is known.
Namespace objectA read-only object-like view of all exports.Use import * as cart to inspect or group exports.
Plain objectAn 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.

A small feature modulePop out in the code editor (opens in a new tab)JavaScript
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.

Load, link, or evaluate?
  • Fetch cart.js after main.js imports it.
  • Notice that cart.js exports count and add.
  • Connect imported count to cart's exported binding.
  • Prepare the cart namespace view for import * as cart.
  • Run console.log("shared") in shared.js.
  • Run add() so the exported count becomes 1.
Try it yourself
0 of 6 correct

Sort each event into the phase that owns it. Read the explanation after every card.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

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.
Similar module words, different jobs
WordWhat it meansDo not confuse it with
LoadingFinding, fetching, and parsing the module graph.Running module statements.
LinkingResolving import names to export bindings.Copying exported values into import variables.
EvaluationRunning a module after dependencies are ready.Always running files in the textual import order.
Namespace objectA 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

Exercise 1 · Warm-upPredict a live value

Read the tiny program. Type the two output lines in order.

Starter codePop out in the code editor (opens in a new tab)JavaScript
let count = 0;
function add() { count += 1; }
console.log(count);
add();
console.log(count);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upName the connection phase

    A teammate asks which phase connects an import to an export. Give the phase name.

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticePredict post-order

      Trace the teaching model and type its comma-separated output.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      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(","));

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeRecognize a namespace

        What string does a module namespace object expose at Symbol.toStringTag?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · PracticePredict the namespace write

          In a module, predict what happens when code writes cart.count = 2 to a namespace export.

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeApply modules to a cart feature

            Your product page needs to add an item and show the cart count. What should it import from a shared cart module?

            Answer, then press Check. Spacing and letter case don’t matter.

              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.

              Load, link, evaluate quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What is a Source Text Module Record in plain words?

                Choose an answer to see the explanation.

              2. Question 2 of 7What does the first live-binding model print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                let count = 0;
                function add() { count += 1; }
                console.log(count);
                add();
                console.log(count);

                Choose an answer to see the explanation.

              3. Question 3 of 7Which phase connects import { count } to the export named count?

                Choose an answer to see the explanation.

              4. Question 4 of 7What does the evaluation model print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const 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.

              5. Question 5 of 7What is cart[Symbol.toStringTag] for a real module namespace object?

                Choose an answer to see the explanation.

              6. Question 6 of 7Can strict module code replace an export through cart.count = 2?

                Choose an answer to see the explanation.

              7. Question 7 of 7Why does the evaluation model keep a visited set?

                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 Module tag.
              • 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.

              CompleteFrontend Clear concepts. Working examples.