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.
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.
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));
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.
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);
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.