Course outline

JavaScript / Intermediate: modules and asynchronous contracts / Lesson 7

Modules and explicit dependency boundaries

Organize calculations and browser adapters with ECMAScript modules. Follow named exports, live bindings, resolution, and host-specific loading without confusing packages with language syntax.

About 4 min readContent reviewed
On this page

Lesson goals

  • Read import/export contracts and module scope.
  • Distinguish live bindings from copied values.
  • Diagnose browser and Node loading assumptions.

A module is a contract between files

Export a small set of intentional capabilities and keep implementation helpers private to module scope. A pricing module can accept validated cents and quantities, while a browser adapter reads fields and displays outcomes. This direction avoids importing document access into reusable policy logic. Named imports must match named exports; a default export is a separate contract rather than a universal synonym for the module. Avoid exporting mutable configuration merely because it is convenient. Review dependencies for cycles and hidden initialization effects, since importing a file can execute its top-level code before the caller starts its own work.

Worked contract: a pure calculation dependency

A normal project would place the exported function in a separate .js module and import it using an explicit relative URL in a browser. The self-contained illustration uses a data URL so the language contract can be tested without creating a server or package. Dynamic import returns a promise for a module namespace object; reading its named property obtains the exported function. This is a local teaching convenience, not a recommendation to embed large application modules in data URLs. Static imports remain clearer for dependencies known when the module is authored.

javascript Self-contained ESM dependency illustration
const source = "export const total = (price, count) => price * count;";
const url = "data:text/javascript," + encodeURIComponent(source);
const { total } = await import(url);
console.log(total(125, 4));
Expected output
500

The function is exported by name and invoked after the import promise settles. The calculation has no browser or Node-specific dependency.

The host resolves a specifier

ECMAScript defines module syntax and linking behavior, but the host determines how a specifier resolves to code. Browsers understand URL-like relative specifiers; bare package names need an import map or a build step that provides a different resolution arrangement. Browser module responses need appropriate JavaScript MIME types and cross-origin policy. Loading an HTML fallback page at a .js path is not a syntax problem in the intended module. Node has its own package, extension, and CommonJS interoperation rules. Do not replace import with require in a browser to bypass an unresolved URL.

Imports observe exported bindings

An imported binding is read-only to the importing module and observes updates made by the exporting module. It is not a snapshot assignment to the importer’s local variable. In the example, the namespace count changes after increment runs. A caller can still mutate an exported object’s properties if the API exposes a mutable object, so read-only binding does not imply immutable state. Favor operations that preserve invariants instead of letting many modules modify shared records directly. For a cycle, an imported binding may be accessed before initialization; restructuring responsibilities is usually clearer than relying on fragile initialization ordering.

javascript A live export observed through its namespace
const source = "export let count = 0; export function increment() { count++; }";
const module = await import("data:text/javascript," + encodeURIComponent(source));
console.log(module.count);
module.increment();
console.log(module.count);
Expected output
0
1

The exporter owns reassignment. The importer observes the later value rather than holding a copied count.

Make initialization and failures reviewable

Prefer an explicit start function for effects such as attaching listeners over hidden work scattered across imported files. Keep module dependencies directional: domain rules do not need the renderer, while the renderer may use domain rules. Test a module through its public exports and validate integration at the adapter boundary. When loading fails, distinguish network retrieval, MIME checking, resolution, linking, and evaluation. A successful download does not prove exports match, and a successfully linked graph can still throw during top-level evaluation. Record the first failing stage before editing syntax or changing package settings.

Common errors and fixes

A browser cannot resolve a bare package name

Why it happens
Package-manager resolution is assumed to be a browser feature.
How to fix it
Use a configured import map, build output, or explicit module URL.
Prevent it next time
Document the loader contract for each host.

An imported value is treated as a snapshot

Why it happens
Live binding semantics are confused with assignment.
How to fix it
Read current exports or create a deliberate snapshot.
Prevent it next time
Test shared state through the exported operations.

A cycle fails during initialization

Why it happens
A module reads another binding before it is initialized.
How to fix it
Separate shared definitions or defer explicit initialization appropriately.
Prevent it next time
Keep the dependency graph directional and review top-level effects.

Check your understanding

1. An import URL returns a 200 response containing HTML. What should you inspect?

Show worked solution

Inspect the response body, MIME type, and server fallback route. The requested module is not being served as JavaScript despite transport success. Correct the path or static serving arrangement, then verify linking and evaluation; changing an export statement cannot repair the wrong resource.

2. Which module should depend on document: price rules or the form adapter?

Show worked solution

The form adapter owns document access and calls pure price rules with validated numeric inputs. This keeps rules portable and easy to test, while browser integration remains an explicit layer with its own missing-node and event-lifecycle checks.

Key takeaways

  • Module syntax is shared; loading and resolution are host contracts.
  • Small exports and directional dependencies make effects visible.

Official references