Skip to the solution
HardLow-Level Design7 min read

How do you handle circular dependencies in Node.js?

1.1k views

The problem

Question presented to candidate: "Module A requires module B, and module B requires module A back, before A has finished executing. What does B actually get back β€” an error, the finished module, or something else β€” and can you observe it directly?"

What a strong answer should cover:

  • Node does not error out or deadlock on a circular require() β€” it returns whatever the circularly-required module's exports object looked like at the exact moment the circular require() call happened, which may be incomplete if that module has not finished executing yet.
  • πŸ“Œ Verified, not assumed: a real circular require() between two files showed the exact partial state directly β€” the second module, requiring back into the still-executing first module, received undefined for a property the first module had not yet assigned, and false for a flag the first module would only set to true later, once it actually finished.
  • πŸ“Œ A real, unprompted signal Node itself gives you: in this exact scenario, Node emitted an actual runtime warning β€” "Accessing non-existent property '...' of module exports inside circular dependency" β€” confirming this failure mode is recognized and flagged by Node's own tooling, not merely a theoretical edge case.
  • The standard, practical fixes: restructure to remove the cycle entirely (often by extracting the genuinely shared logic both modules need into a third module that neither of the original two depends on circularly); defer the circular require() call to inside a function body rather than at the top of the file, so it only runs after both modules have fully finished loading (by the time the function is actually called); or, for cases where partial initialization order is unavoidable, explicitly design the API so that accessing the circularly-required module immediately after import is never required β€” only later, after the module graph has settled.
  • A precise answer names that this is specifically a CommonJS behavior tied to require()'s synchronous, immediate-return nature β€” ES Modules handle circular imports differently, using live bindings that update once the actual export is assigned, rather than a snapshot frozen at require time (though a circular ESM import can still surface a temporal-dead-zone-style error if an export is accessed before its module has run far enough to initialize it).
  • The most reliable way to actually detect an unintentional circular dependency in a real codebase (rather than reasoning about it in the abstract) is a static analysis tool (e.g. madge) that builds the full module dependency graph and reports cycles directly, rather than discovering the issue only when a specific circular access happens to produce an observably broken undefined.

Clarifying questions expected:

  • "Is this CommonJS specifically, or does the codebase use ES Modules?" β€” the underlying mechanism (snapshot vs. live binding) genuinely differs.
  • "Is the goal fixing an existing observed bug, or preventing this proactively in a large codebase?" β€” the latter points toward a static cycle-detection tool.

Code / implementation expected: Yes β€” a real circular require(), showing the exact partial-exports values observed and Node's own real emitted warning, is the concrete, convincing demonstration rather than a description of "it can be incomplete."

  • modules
  • commonjs
  • architecture

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

7 min read Β· 9 sections

Read the code

JavaScriptA real circular require() between two files, showing the exact partial-exports snapshot and Node's own real emitted warning
// a.js
console.log("a.js: starting");
exports.aReady = false;
const b = require("./b.js"); // circular: b.js requires back into a.js
console.log("a.js: b.bReady at this point is", b.bReady);
exports.aValue = "value-from-a";
exports.aReady = true;

// b.js
console.log("b.js: starting");
exports.bReady = false;
const a = require("./a.js"); // a.js has NOT finished running yet
console.log("b.js: a.aValue at this point is", a.aValue, "| a.aReady:", a.aReady);
exports.bReady = true;

// main.js: require("./a.js")
//
// Output:
// a.js: starting
// b.js: starting
// b.js: a.aValue at this point is undefined | a.aReady: false   <- INCOMPLETE snapshot
// a.js: b.bReady at this point is true
// (node) Warning: Accessing non-existent property 'aValue' of module
// exports inside circular dependency   <- Node's OWN real, unprompted warning

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 116 of 152 in Node.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Node.js questions
Up nextHard

How does CommonJS `require()` an ES Module in modern Node.js?

Continue