In Node's CommonJS, a.js sets `exports.done = false`, then calls `require('./b.js')`, and b.js immediately calls `require('./a.js')` and reads `.done` on the result. What does b.js receive, and why is no error thrown?
answer
- cached before the body executes
- cache hit, so nothing throws
- half-built exports, silent undefined
- assignment position decides visibility
- replacing module.exports breaks the reference
basics
~20 sb.js receives a.js's partially populated exports object: done is false, and anything a.js assigns after its require call is simply absent. CommonJS caches the module object before running the body, so the back-edge require returns whatever exists at that instant — silently.
solid answer
~40 s`require` resolves the specifier, creates the module object, inserts it into `require.cache` keyed by the resolved filename, and only *then* runs the file. So when `b.js` requires `a.js` back while `a.js` is still executing, the cache already holds `a.js`'s module object and returns its current `exports` — `{ done: false }`. Nothing throws, because from CommonJS's point of view this is an ordinary cache hit; there is no notion of "unfinished". Properties assigned before the `require` call are visible, properties assigned after are missing and read as `undefined`. That silence is the real hazard: an `undefined` flows downstream and fails somewhere unrelated. It gets worse when the module does `module.exports = something` after the back-edge, because the partner captured the *original* object and never sees the replacement.
code
javascript · 15 lines// a.js
console.log('a starting');
exports.done = false;
const b = require('./b.js');
console.log('in a, b.done =', b.done);
exports.done = true;
console.log('a done');
// b.js
console.log('b starting');
exports.done = false;
const a = require('./a.js');
console.log('in b, a.done =', a.done); // false — a.js is still executing
exports.done = true;
console.log('b done');go deeper
Know that require never errors just because two files require each other; the risk is that you receive an exports object that has not finished being filled in.
Describe the cache-before-execute ordering inside require, and predict exactly which properties are present at the back edge given where the assignments sit relative to the require call.
Point at the silence as the real danger — an undefined property flows on and fails far from the cycle — and show the lazy-require or extract-a-third-module repairs, with their tradeoffs.
Weigh whether legacy CommonJS cycles get tolerated or forced open, knowing the failure is invisible until a consumer misbehaves in production and that migration to ESM converts silence into a hard throw.
## What require actually does A `require(specifier)` call performs a fixed sequence: resolve the specifier to an absolute filename, check `require.cache` for that filename, and on a miss create a new module object with an empty `exports` object, **insert it into the cache**, and then compile and execute the file. The insertion happening *before* execution is the entire reason cycles terminate — and the entire reason they misbehave. When `b.js` requires `a.js` while `a.js` is mid-body, the cache lookup hits. CommonJS has no concept of a module being "in progress"; a hit is a hit, and it returns the object as it stands. There is nothing to throw about. ## The canonical trace ```js // a.js console.log('a starting'); exports.done = false; const b = require('./b.js'); console.log('in a, b.done =', b.done); exports.done = true; console.log('a done'); // b.js console.log('b starting'); exports.done = false; const a = require('./a.js'); console.log('in b, a.done =', a.done); exports.done = true; console.log('b done'); ``` Requiring `a.js` from a third file prints: `a starting`, `b starting`, `in b, a.done = false`, `b done`, `in a, b.done = true`, `a done`. Read the asymmetry: `b.js` saw `a.done === false` because `a.js` had not reached its second assignment yet, while `a.js` saw `b.done === true` because `b.js` ran to completion before control returned. The module that gets the incomplete view is always the one that is *deeper* in the require chain. ## Position of the assignment decides what is visible Everything hinges on where the exporting assignment sits relative to the `require` that closes the cycle. Move `exports.done = true` above the `require('./b.js')` line and the partner sees `true`. That is why cycle bugs in CommonJS feel maddeningly arbitrary: adding one more line of setup below a require can flip a downstream value from a working object to `undefined`. ## The module.exports reassignment trap Assigning *properties* onto `exports` at least lets the partner see whatever landed so far, because both modules hold the same object. Replacing the object breaks even that: ```js // a.js const { helper } = require('./b.js'); // b.js requires a.js back here module.exports = class Widget { /* ... */ }; ``` At the moment `b.js` requires `a.js` back, `a.js`'s `module.exports` is still the original empty object, and that is the reference `b.js` captures. When `a.js` later assigns a class to `module.exports`, it swaps in a *different* object; `b.js`'s captured reference still points at `{}` forever. The symptom is a `TypeError: Widget is not a constructor`, or a method that is `undefined`, in a file that looks entirely innocent. Destructuring at require time — `const { helper } = require('./b.js')` — has the same problem in miniature: it snapshots a property that may not exist yet, so even a later fix to the exports object never reaches the captured variable. ## Deferring the require Because `require` is an ordinary function call, not static syntax, you can move it inside the function that needs it: ```js // b.js function render() { const { Widget } = require('./a.js'); // resolved on first call, after a.js finished return new Widget(); } ``` By the time `render()` is called, `a.js` has finished and the cache holds a complete exports object. This is a genuine repair for the crash, and it is common in legacy CommonJS code — but it hides the cycle rather than removing it, and it moves a resolution failure from startup to whenever that code path first runs. ## Diagnosing it The tell is an imported value that is `undefined` (or an empty object) with no error near the point of use, combined with sensitivity to load order: it works when one file is required first and breaks under another entry point. Logging `Object.keys(required)` right after the require, or printing `module.parent`-style require chains, quickly shows that the object arrived incomplete rather than never having been populated at all.
- Why does `module.exports = class Foo {}` make a cycle worse than assigning properties onto `exports`?Because the partner captured a reference to the *original* exports object when the cycle closed. Assigning properties mutates that shared object, so the partner eventually sees them. Replacing `module.exports` installs a different object the partner never learns about, so its reference stays `{}` forever — typically surfacing as `TypeError: Foo is not a constructor`.
- How does moving the require call inside a function change the outcome?It defers resolution to call time, after both modules have finished evaluating, so the cache returns a complete exports object. It genuinely stops the crash and is common in legacy CommonJS. The cost is that the cycle stays in the graph and a resolution failure now surfaces on first call instead of at startup.
- From a debugging standpoint, how does the CommonJS failure differ from the ES module one?CommonJS is silent: you get `undefined` that travels until something unrelated blows up, so the stack points nowhere near the cycle. ESM throws `ReferenceError: Cannot access 'x' before initialization` at the exact offending read. The ESM behaviour is more hostile at first contact and far cheaper to diagnose.
- Why is destructuring the result of a require riskier inside a cycle?Destructuring copies the property value at require time. Inside a cycle that value may not have been assigned yet, so you capture `undefined` permanently — later mutations of the exports object never reach your variable. Holding the namespace object and reading the property at use time at least sees the finished value.
Requiring a module mid-cycle is like reaching into a kitchen and taking the mixing bowl while the recipe is still running: you get whatever ingredients have been added so far, and nobody tells you the rest was coming.
saying these in an interview costs you the question
- require throws when it detects a circular dependency
- The back-edge require re-runs the module from the top
- CommonJS gives live bindings like ESM does
- Where you assign exports relative to require makes no difference
- Destructuring the require result is safe inside a cycle