Skip to solution
hardLow-Level Design

How do you handle circular dependencies in Node.js?

1.1k views
01

Understand 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."

modulescommonjsarchitecture
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for Node.js interviews — assumes basic require()/module-caching familiarity (see the dedicated modules question). Difficulty: Hard

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. The circular-re

Solution ready — 2 min read

Classified // press E to declassify

04

Read the code

A 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
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 116 of 152 decoded in the Node.js track. One more won't hurt.

Back to track