Glossary / Circular dependencies
Circular dependencies
A circular dependency exists when module A depends directly or indirectly on module B and B's dependency path leads back to A. ESM and CommonJS support cycles, but their initialization semantics differ; code that reads exports too early can observe uninitialized values, incomplete objects, or fragile order-dependent behavior.
ESM cycles
ESM links the full graph before evaluation and uses live bindings. A cycle can work when modules access each other's exports after initialization, but reading a let, const, or class export before initialization throws ReferenceError because of the temporal dead zone. Function declarations may be initialized earlier in module instantiation, but relying on that detail is brittle.
CommonJS cycles
CommonJS executes modules synchronously and caches them before completion to break recursive loading. A cyclic require() can therefore receive the current, partially populated module.exports object. Reassigning module.exports later does not update the earlier reference. Behavior may depend on import order because partial state differs.
Diagnosis and remediation
Cycles often signal responsibilities crossing boundaries. Detect strongly connected components in the dependency graph and inspect which values are read during initialization. Break cycles by extracting shared contracts/constants into a lower-level module, inverting dependencies through callbacks or interfaces, moving work from import time into explicit functions, or consolidating tightly coupled modules. “Barrel” re-exports can create unexpected graph cycles.
Test cold module startup, alternate entry points, and production bundler output. A build tool may reorder, flatten, or transform modules and expose issues hidden by source-level tests. Do not fix a cycle with arbitrary delays or dynamic imports without clarifying ownership and initialization order.
Interview questions
- Are cycles forbidden in ESM? No, but access to uninitialized lexical exports can fail.
- Why might CommonJS see an incomplete module? The module is cached before execution finishes.
- How do you break a cycle? Extract shared abstractions, invert dependencies, or move side effects into explicit initialization.
- Why do import-time side effects matter? They make evaluation order observable and increase sensitivity to cycles.
- How investigate? Analyze the graph and reproduce with the production loader/build and alternate entry points.