Skip to content
tezvyn:
Menu

Circular dependencies in CommonJS modules

Source: interviewHardHow cards are made

Summary

deep understanding of CommonJS loading.

Key points

when A requires B which requires A, the cache returns A's partial exports; fields defined later are undefined at that moment.

What's really being asked

This probes whether you truly understand the mechanics of CommonJS loading and caching, since circular dependencies fail silently and subtly rather than loudly.

The full answer

A circular dependency exists when module A requires module B and B, directly or through a chain, requires A. CommonJS handles this without throwing by relying on its module cache. When a module starts loading, Node immediately places its exports object, initially empty, into the cache. If during A's execution it requires B, and B then requires A, Node sees A already in the cache and returns A's current exports rather than re-running A. The catch is that A may not have finished assigning its exports yet, so B receives a partially populated object. Any property A defines after the point where it required B will be undefined from B's perspective at that moment.

The mistakes people make

Claiming Node throws an error or deadlocks on cycles, asserting exports are always fully populated, or believing ESM behaves identically, when ESM uses hoisted live bindings that handle some cycles more gracefully but can still throw temporal dead zone errors.

What usually comes next

How to break a cycle by moving shared logic to a third module, why ordering the require statement after assignments can help, how lazy require inside functions defers resolution, and how ESM live bindings differ.

A concrete example

Suppose a.js sets module.exports.first then requires b.js, then sets module.exports.second. Inside b.js it requires a.js and reads both properties at load time. Because a.js had only assigned first before requiring b.js, b.js sees first defined but second undefined, even though a.js later completes. If b.js instead reads second lazily inside a function called after both modules finish loading, it sees the complete object, which is the common workaround.

Interview question

When module B requires module A in the middle of A's own loading, what does B receive?

  • a.A freshly re-executed copy of A's exports
  • b.An error, since CommonJS forbids cycles
  • c.A's exports object as populated so far, possibly incompleteCorrect
  • d.An empty object that updates synchronously when A finishes
Why?

CommonJS caches the exports object at load start and returns it as-is, so B gets whatever A has assigned up to that point. It is not re-executed, does not throw, and the reference is not retroactively backfilled.

Just read this? Test yourself on what you have been reading.

Read the original → nodejs.org

Put your scrolling time to good use

Learn one idea, try a quiz and save useful cards for revision. Tezvyn makes it easy to learn and stay current in your tech field, a few minutes at a time.

The iPhone app is on the way

We are building it. Until it lands, nothing here is held back from you: every interview card, your saved cards, streaks and the job board all work in Safari, plus hundreds of free practice quizzes of thirty questions each. Sign in and it all carries over to the app the day it arrives.

Want it as an icon? Tap Share at the bottom of Safari, then Add to Home Screen. It opens full screen and the cards you have read stay available offline.

Get it on Google PlayiPhone app coming soon

We are hiring for this. Open roles that interview on nodejs — each one lists the topics its interview covers.

See open roles