Node does not loop forever. Instead, one module gets an unfinished exports object.
Step by step:
- When
a.jsstarts, Node creates itsmodule.exportsobject and caches it right away. a.jsrequiresb.js.b.jsrequiresa.jsback. Node returns the cached, unfinished exports ofa.js.- So
b.jssees only whata.jsexported before itsrequire('./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
exportsinstead 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.exportslater 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.
Read more