Modules and Bundling
How JavaScript source is split across files and linked back together — ES modules, CommonJS, and what the two do differently at load time. Children cover ESM semantics, dynamic import and code splitting, ESM/CJS interop, circular dependencies, and side effects with tree shaking.
part ofJavaScriptoverview, primer and where to startread it →on this pageshowhide
explore
- ES Module Semantics17 questions
- Import and Export Forms4 questions
- Live Bindings and Namespace Objects4 questions
- Static Structure and Evaluation Order5 questions
- Module Scope and Strict Mode4 questions
- Dynamic import() and Code Splitting5 questions
- Top-Level Await5 questions
- ESM and CommonJS Interop12 questions
- CommonJS Module Semantics4 questions
- Default Export Interop Mismatch4 questions
- Dual-Package Hazard4 questions
- Circular Dependencies4 questions
- Side Effects and Tree Shaking6 questions
questions
page 2 of 2Why can a build tool usually not eliminate unused exports from a CommonJS module the way it can from an ES module?
basics
~20 sCommonJS builds its exports at runtime: require is an ordinary function call and module.exports is a mutable object whose properties can be added conditionally and read with computed keys. The export set is not knowable before execution, so tools keep the whole module.
A large JavaScript codebase intermittently sees an imported value arrive as undefined, or throws 'Cannot access before initialization', depending on which entry point loads first. How do you confirm an import cycle is the cause, and how do you break it?
basics
~20 sMap the import graph and look for a loop — a tool such as madge run with --circular, or ESLint's import/no-cycle rule, lists them mechanically. Then break the loop by extracting the shared code into a third module, or by inverting the dependency, rather than by reordering imports.
A CommonJS service takes several seconds to start serving traffic, and the time is spent in top-level code of required modules — parsing config, building lookup tables — before the server ever starts listening. Why does require() put that cost exactly there, and how would you restructure the modules?
basics
~20 srequire() runs a module's body to completion synchronously before returning, so anything at a module's top level executes during the require call and blocks the single thread. Move expensive work into exported init or factory functions the application calls deliberately.
In a Node service, `err instanceof ValidationError` returns false for errors thrown by the very library that exports `ValidationError`, even though `err.constructor.name` is "ValidationError" and only one version of the library is installed. Parts of the codebase load the library with `import`, others with `require`. What is happening, and how would you confirm it?
basics
~20 sTwo builds of the same library are loaded, so two distinct ValidationError constructors exist. The error came from one copy and is being tested against the other copy's class, and instanceof compares prototype identity, not names. Confirm it by comparing the file paths each side actually resolved.
You maintain a JavaScript package that ships both a CommonJS and an ESM build and keeps a module-level registry of plugins. What can you change so consumers can never end up with two independent registries, and what does each option cost them?
basics
~20 sGuarantee a single implementation. Either ship one build and make the other entry a thin wrapper that re-exports it, move all stateful pieces into one internal file both builds load, or publish a single format. Each trades bundle quality or consumer convenience for guaranteed single-instance state.
A single-page app lazy-loads a route with `await import('./routes/settings.js')`. In production, some users hit a blank screen because that load fails. How does `import()` surface the failure, and how would you handle it in the code?
basics
~20 simport() reports failure by rejecting its promise — nothing is thrown at the call site. Wrap the await in try/catch, distinguish a failed fetch from an error thrown while the module evaluates, show a real error state, and recover from a stale-deploy 404 with a full page reload rather than a bare retry.
When designing an ES module's public surface, what are the practical tradeoffs between exposing values as named exports versus a single default export?
basics
~20 sNamed exports keep the public name under the author's control, so identifiers stay consistent, greppable and safely renameable by tooling. A default export makes a one-artifact file read cleanly but hands the naming decision to every importer, which invites drift.
A module has `export let ready = false;` and sets `ready = true` during startup. Consumers using `import { ready }` see it become true, but after the same code is compiled to CommonJS and consumers write `const { ready } = require('./m')`, they see false forever. What explains the difference, and how would you export this value so it works under both?
basics
~20 sAn ES module import is a live binding to the exporter's variable, while destructuring a CommonJS require copies the property's value at that instant. Export a function or a state object so consumers read the value at access time instead of capturing it.
A JavaScript file that worked when loaded as a classic script now throws ReferenceError once the same code is loaded as an ES module — once on a helper function another file defines, and once on a line that assigns to a name without declaring it. What changed, and how do you fix each?
basics
~20 sTwo module rules bit at once. Module scope means the other file's helper is no longer visible as a bare name, so it must be exported and imported; implicit strict mode turns the undeclared assignment from a silent global into a ReferenceError, so declare it with const or let.
A module counts how many times its top-level code runs and five different modules import it. How many times does the body actually run, what identifies a module instance, and how can a codebase end up with two separate instances of what looks like the same module?
basics
~20 sOnce. A module's body is evaluated exactly one time per instance, and instances are keyed by the resolved location of the module, so all five importers share one instance. Two instances appear when the same source resolves to two different locations — a duplicated package copy, a differing specifier, or a separate realm.
In Node's native ESM, `import { parse } from 'legacy-cjs-pkg'` fails with `SyntaxError: The requested module 'legacy-cjs-pkg' does not provide an export named 'parse'`, yet `require('legacy-cjs-pkg').parse` is a function at runtime. Explain what causes that mismatch and how you would fix the import site.
basics
~20 sNamed exports from a CommonJS module are discovered by statically scanning its source before it runs, so properties attached dynamically are invisible to the linker even though they exist at runtime. Default-import the module and destructure the property afterwards.
Two ES modules import each other, and one of them top-level awaits a value the other only produces after its own body finishes. What happens when the application loads, and how would you track it down?
basics
~20 sNothing happens: the graph's evaluation promise never settles, so the modules stay half-evaluated and the entry point never runs. There is no error, no stack trace and no timeout — the load simply hangs, which is why it is diagnosed by tracing where logging stops.
What does `"sideEffects": false` in a package's package.json actually claim, and what goes wrong when a package makes that claim untruthfully?
basics
~20 sIt claims that no file in the package does anything observable merely by being evaluated, so a bundler may skip importing any module whose exports go unused. It is an unverified promise; a false one silently deletes polyfills, styles and registrations from production builds.
When deciding where to put `import()` split points in an application, what makes a call site a good boundary, and what goes wrong when a team splits too aggressively?
basics
~20 sGood boundaries sit where a module is both large and genuinely optional, gated behind a user intention the app can anticipate. Over-splitting fragments code into many small units, creates sequential request waterfalls when one lazy module lazily imports another, and shifts latency onto interactions.
When is top-level await in an ES module the right way to do async initialization, and when would you instead export an initializer function or a promise?
basics
~20 sUse top-level await when a module's export is meaningless until an async step completes and every consumer needs it — it makes correct ordering structural. Prefer an exported initializer or promise when the module is widely imported, when the work can fail or needs retry control, or when consumers must load synchronously.
In an ES module, `import * as ns from './m.js'` gives you `ns`. What kind of object is `ns` — can you add, delete or overwrite properties on it, and what is its prototype?
basics
~20 sns is a module namespace object: non-extensible, with one non-configurable property per export whose value is a live view of the exporter's variable. Adding, deleting or overwriting properties is rejected, and its prototype is null.
In a library's published output you find `const registry = /*#__PURE__*/ createRegistry();`. What does that comment assert, what reads it, and what is the risk of adding one?
basics
~20 sIt asserts that the immediately following call has no observable effects, so if its result is unused the whole call may be deleted. Minifiers and bundlers read it. Nothing verifies it, so a wrong annotation silently removes real behaviour from production builds.
As the maintainer of a widely depended-on JavaScript library, how would you decide whether to ship an ESM build only, a CommonJS build only, or both, given the dual package hazard?
basics
~20 sDecide by what your public API depends on. If correctness rests on shared state or class identity, ship a single implementation so two copies are impossible. If the library is stateless, dual builds are a bundle-size convenience with no correctness risk, and the choice becomes purely a compatibility question.
Parts of a large codebase rely on module top-level side effects — plugins that self-register on import, a logger configured at import time — so correctness depends on which modules are imported and in what order. As the engineer setting the standard, how do you evaluate that pattern and what would you put in its place?
basics
~20 sTreat evaluation-order dependence as hidden coupling: the ordering contract lives in import statements no reviewer reads as ordering. Prefer module bodies that only declare things, plus one composition root that constructs and wires everything in an order you can read, test, and change.
showing 31–49 of 49