CodePark/ questions / Modules (CommonJS & ESM)
START PRACTICINGPRACTICE
Backend question · Node.js › Modules (CommonJS & ESM)

What happens with a circular dependency in CommonJS, where a.js requires b.js and b.js requires a.js?

Node does not loop forever. Instead, one module gets an unfinished exports object.

Step by step:

  • When a.js starts, Node creates its module.exports object and caches it right away.
  • a.js requires b.js.
  • b.js requires a.js back. Node returns the cached, unfinished exports of a.js.
  • So b.js sees only what a.js exported before its require('./b') line. This is often an empty object.

This causes bugs like TypeError: x is not a function that depend on load order. It is worse if a.js later replaces module.exports with a new object. Then b.js keeps the old object forever.

Fixes:

  • Restructure the code so shared code lives in a third module.
  • Require lazily, inside the function that needs it.
  • Add properties onto exports instead of replacing it.

ES modules handle cycles with live bindings: an import always reads the current value of the export. But reading a binding before it is initialized throws a ReferenceError.

  • Node caches the exports object before the module finishes running.
  • The module in the cycle gets a partly filled exports object.
  • Replacing module.exports later leaves old references behind.
  • Fix it by moving shared code to another module or by requiring lazily.
  • ESM uses live bindings but can hit TDZ (temporal dead zone) errors when a binding is read too early.