Skip to main content

What is a "circular dependency"?

1. Definition

A circular dependency is a situation where two (or more) modules depend on each other, directly or indirectly, forming a closed loop of dependencies.

In other words:

  • Module A imports module B
  • Module B imports module A

Or worse, through a chain:

javascript
ABCA

2. A simple example in Node.js

a.js

javascript
const b = require('./b'); console.log('Module A loaded'); module.exports = 'A';

b.js

javascript
const a = require('./a'); console.log('Module B loaded'); module.exports = 'B';

main.js

javascript
const a = require('./a'); const b = require('./b');

The result at runtime:

javascript
Module B loaded Module A loaded

Notice: the order is not what you'd expect.

3. What actually happens under the hood

When Node.js hits a require(), it:

  1. Starts loading module a;
  2. Sees that a requires b;
  3. Starts loading b;
  4. But b also requires a, and a hasn't finished loading yet;
  5. Node.js hands b a partially loaded (incomplete) a object.

So with a circular dependency, one of the modules ends up with an incomplete export (undefined or a partial object).

4. Demonstrating a partially loaded module

a.js

javascript
exports.loaded = false; const b = require('./b'); console.log('b.loaded =', b.loaded); exports.loaded = true; console.log('a loaded');

b.js

javascript
exports.loaded = false; const a = require('./a'); console.log('a.loaded =', a.loaded); exports.loaded = true; console.log('b loaded');

main.js

javascript
require('./a');

Output:

javascript
a.loaded = false b loaded b.loaded = true a loaded

In other words, when b imported a, it got an unfinished object, a.loaded was false.

5. Why this happens

Node.js caches every module on require(). As a module loads, it's added to require.cache, even if it hasn't finished executing.

So with a circular dependency, one of the modules "sees" an under-loaded version of the other.

6. In real life

These cycles often appear not directly, but through a chain of imports:

javascript
A → utils.js → logger.jsA

or in an architecture like:

javascript
controller → service → model → controller

This is usually a sign of poor design (modules too tightly coupled).

7. Signs you have a "circular dependency"

  • An import returns undefined;

  • Code runs in an unexpected order;

  • Node.js prints a warning at startup:

    javascript
    Warning: Accessing non-existent property 'X' of module exports inside circular dependency
  • Behavior is unstable or differs between environments.

8. How to avoid circular dependencies

Option 1: break the cycle with a third module

Create a "shared" module that holds what both need.

Bad:

javascript
ABA

Good:

javascript
ACommon BCommon

common.js

javascript
module.exports = { sharedValue: 42 };

a.js

javascript
const common = require('./common');

b.js

javascript
const common = require('./common');

Option 2: use lazy loading (require() inside a function)

If a module is only needed when a function is called, require() it later:

javascript
function useB() { const b = require('./b'); // loads only when called b.doSomething(); }

This breaks the "moment of the cycle" at app startup.

Option 3: change the architecture

  • Split responsibilities between modules;
  • Get rid of "mutual" dependencies, let one module own the other;
  • Use dependency injection.

Option 4: use ESM (ES Modules)

In ES Modules (with import/export), circular dependencies resolve correctly, because imports create live bindings, not static values like in CommonJS.

An example (works reliably):

javascript
// a.mjs import { b } from './b.mjs'; export const a = 'A'; console.log('a sees b =', b); // b.mjs import { a } from './a.mjs'; export const b = 'B'; console.log('b sees a =', a);

9. Intuitively: why this is a problem

Picture two files launching each other at the same time:

javascript
A launches BB launches AA launches B...

To avoid going on forever, Node.js "stops" the cycle and returns whatever it managed to load at that point. That's why you see "half an object".

10. Tools for finding cycles

For a large project, there are utilities:

javascript
npm install madge -g madge src/

It shows which files form circular dependencies.

Quick summary

TermMeaning
Circular dependencyModules import each other (directly or through a chain)
CauseA module requires another one that hasn't finished running yet
Consequenceundefined or a partially loaded module comes back
FixesSplitting logic, lazy imports, refactoring, ESM

In one sentence

A circular dependency is a situation where Node.js modules import each other, forming a closed loop, so one of them loads incompletely, which leads to errors and undefined on require().

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.