Neither system rejects a cycle. What differs is what a module sees from a partner that hasn't finished.
ES modules. Linking handles the whole cycle as a unit, so every import binding exists before any code runs. Evaluation is depth-first: starting from the entry module, a module that is already being evaluated is skipped rather than waited for. So the last module in the cycle to be reached runs first, and the entry module runs last.
When a module's body reads a binding that comes from a module that hasn't run yet:
export functiondeclarations already work, because functions are initialized at link time.export const,let, andclassare in their Temporal Dead Zone: aReferenceError, "Cannot access 'a' before initialization".export varreadsundefined.
Reading the binding later, for example inside a function called after startup, works because the binding is live.
CommonJS. require returns the module.exports object that exists at that moment. If the partner is mid-execution, you get whatever it had assigned so far, usually a partial object. A property added later shows up on the same object, but destructuring (const { x } = require(...)) or replacing module.exports freezes in the early view.
The fix is usually structural: move the shared code into a third module, or read the other side lazily, inside a function.