skip to content

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?

level: middleimportance: should knowfreq 48%

answer

  1. cached before the body executes
  2. cache hit, so nothing throws
  3. half-built exports, silent undefined
  4. assignment position decides visibility
  5. replacing module.exports breaks the reference

basics

~20 s

b.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
javascript
// 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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

context