← JavaScript: language and application diagnosis
09 / 10 · 50 MIN

Modules and runtime boundaries

Interpret imports and exports as contracts between modules; distinguish live bindings, snapshots and host loading rules.

Local names and exported contract

A named export is identified by its published name. import { parse as read } declares read locally but still requests the parse export. A default export occupies the default entry and can be received under another local name, such as import render from "./format.js". Declaring export default function format does not, by itself, create a second named format export.

import * as config supplies a namespace whose properties expose exports. That object is neither the default function itself nor a bag of freely editable values. When diagnosing an import, separate three questions: was the module found, did it publish the requested name, and does that value meet the consumer contract? Changing the local name fixes neither a missing path nor a missing export.

Live bindings and explicit snapshots

A named import indirectly links to the exporter binding. If the exporter reassigns an exported let, a later read through the import observes the new value. The consumer cannot directly reassign that imported binding. However, when the exported value is an ordinary object, the binding restriction does not freeze its properties: another consumer can observe changes to that shared object.

const initial = rate copies the value read at that instant into a local variable. If rate is a Number, initial retains that number after the exporter changes rate. If the value were an object, copying only the reference would still share the object. Decide whether the consumer needs current state or an independent snapshot and construct that contract deliberately.

Initialization, strict mode and cycles

Modules use strict mode, and their top-level this is undefined. Names defined in a module do not automatically become window properties. Static imports belong at module top level; they are not declarations to place inside an if or function. The dependency graph participates in linking and evaluation before the consumer executes its ordinary body.

In a cycle, one module can attempt to read a binding the other has not initialized. That read can throw ReferenceError even though the name is correctly exported. Not every cycle fails: use timing is decisive. A sustainable correction may extract the shared dependency or make initialization explicit. Adding an arbitrary delay to hide ordering does not clarify the contract and can create another timing failure.

Resolve and deliver the browser module

In a browser without an import map or special loader, "./lib/parse.js" resolves relative to the importing module URL. For /app/main.js it points to /app/lib/parse.js. A bare package name such as "parser-lib" needs a mapping or another resolving tool; the browser does not automatically adopt another host package lookup. Specifier resolution is a host responsibility.

A script element with type="module" declares the entry as a module. Defer alone does not turn a classic script into a module. Also inspect the resource actually delivered: HTTP 200 with Content-Type text/html and a home-page body can be a server fallback for a missing path. A JavaScript module needs suitable content and MIME type. HTTP status, resource type and exports are different checks.

Dynamic loading and host dependencies

import("./report.js") is an expression returning a promise; on success, the consumer receives the module namespace. It can serve an optional application path, but the caller remains responsible for tracking rejection and checking the export it uses. It neither immediately returns the default function nor merely reads source code as a string.

Module format does not supply a missing DOM, filesystem or other API. If a calculation rule reads document during initial evaluation, a consumer without DOM fails before it can use the rule. Receiving data as arguments and keeping host reading in an adapter allows logic reuse. Dynamic import can defer evaluation, but it does not eliminate that dependency when the module is eventually used.

Guided example: current policy and retained value

The three example files share the same state.js module. main.js reads limit while it is 10 and stores that Number in originalLimit. Calling accepts(12) returns false. setLimit(20) validates and reassigns the exported binding. The later accepts(12) call returns true because policy.js reads current limit through its import. At the end, originalLimit is 10 and limit is 20.

If setLimit receives the string "20", Number.isInteger rejects it and assignment does not occur. There is no implicit conversion. The policy accepts nonnegative integer units up to the current limit; it does not validate other business dimensions. Shared state is intentional in this example. For decisions needing historical reproducibility, passing an explicit limit captured at approval creates a different, more suitable contract than reading the current value later.

A traceable diagnostic sequence

Start with execution context: native module or classic script, host with or without DOM, and the tool resolving names. Then follow the URL or specifier to the received resource. Only after confirming delivery and type does it make sense to diagnose export name, initialization and value use. This sequence avoids rewriting a correct function when the server delivered HTML instead of the file.

For shared state, record read timing and who can write. A live import, a primitive copy and a shared reference are not equivalent. In cases, select the change addressing the boundary demonstrated by the facts. Running the files is unnecessary: excerpts and premises identify the relevant contracts and ordering.

// state.js
export let limit = 10;
export function setLimit(value) {
  if (!Number.isInteger(value) || value < 0 || value > 50) {
    throw new TypeError("limit");
  }
  limit = value;
}

// policy.js
import { limit } from "./state.js";
export function accepts(units) {
  return Number.isInteger(units) && units >= 0 && units <= limit;
}

// main.js
import { limit, setLimit } from "./state.js";
import { accepts } from "./policy.js";
const originalLimit = limit;
const before = accepts(12);
setLimit(20);
const after = accepts(12);
IN PRACTICE

Comments delimit three separate files, not one file to concatenate. Assume native ES modules, the same resolved URLs for state.js, standard methods and no other changes. Predict originalLimit, limit, before and after following main.js evaluation.

Common pitfalls

Treating import as a copy; confusing default and named exports; assuming an extension supplies APIs; reading cyclic bindings before initialization; accepting HTTP 200 as proof of correct module delivery.

Related topics: Bindings, functions and closures · Objects, identity and property boundaries · Promises, async and error propagation · State, ordering and integration diagnosis

Take this idea with you

A module contract includes exported names, identity and read timing alongside resolution and host-provided APIs.

Create account

Reference: CreateImportBinding · ECMAScript 2026 (17th edition); browser APIs identified separately