Circular Dependencies
A circular dependency occurs when module A requires module B which requires module A. Both CJS and ESM handle this, but in different ways that can cause subtle bugs.
Circular deps are two people each waiting for the other to finish talking before they speak. CJS handles it by giving you a half-written letter (partial exports). ESM gives you a sealed envelope that's empty until the writer finishes—open it too early and it explodes (ReferenceError).
In CJS, when a circular require() is detected, Node returns whatever the required module has exported so far (a partially-constructed exports object). If module A requires B at the top level and B requires A, B gets A's exports as they were at the point A triggered B's load—typically an empty object. Code that relies on A's exports only later (inside functions, not at module evaluation time) works fine; top-level use of A's exports in B will see the empty object.
ESM handles circulars differently via live bindings: exports are references that get populated during evaluation. A circular ESM import doesn't get a snapshot—it gets the live binding. If module A exports `let x = 1` and module B imports x before A's code runs, accessing x throws a ReferenceError (TDZ). But if B only accesses x inside a function called after A has initialized, it works. The practical fix for both systems is to restructure to break the cycle (extract shared code to a third module) or use dependency injection. Tools like madge visualize the dependency graph to identify cycles.
CJS circular deps return a partial exports object—code relying on top-level exports of a cyclically-imported module sees an empty object. ESM circular deps use live bindings, which avoids the partial object problem but introduces TDZ errors if a binding is accessed before its providing module finishes evaluating. The safest resolution is restructuring to eliminate cycles by extracting shared code.
A common CJS circular dep bug: module A exports a class, module B imports it at the top level (for instanceof checks) and also re-exports something A needs. A loads B as part of its initialization; B's top-level require(A) returns A's partial exports (no class yet). Now every instanceof check in B silently fails because the class is undefined. The bug is invisible until you run specific code paths.