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:
A → B → C → A2. A simple example in Node.js
a.js
const b = require('./b');
console.log('Module A loaded');
module.exports = 'A';b.js
const a = require('./a');
console.log('Module B loaded');
module.exports = 'B';main.js
const a = require('./a');
const b = require('./b');The result at runtime:
Module B loaded
Module A loadedNotice: the order is not what you'd expect.
3. What actually happens under the hood
When Node.js hits a require(), it:
- Starts loading module
a; - Sees that
arequiresb; - Starts loading
b; - But
balso requiresa, andahasn't finished loading yet; - Node.js hands
ba partially loaded (incomplete)aobject.
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
exports.loaded = false;
const b = require('./b');
console.log('b.loaded =', b.loaded);
exports.loaded = true;
console.log('a loaded');b.js
exports.loaded = false;
const a = require('./a');
console.log('a.loaded =', a.loaded);
exports.loaded = true;
console.log('b loaded');main.js
require('./a');Output:
a.loaded = false
b loaded
b.loaded = true
a loadedIn 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:
A → utils.js → logger.js → Aor in an architecture like:
controller → service → model → controllerThis 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:
javascriptWarning: 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:
A → B → AGood:
A → Common
B → Commoncommon.js
module.exports = { sharedValue: 42 };a.js
const common = require('./common');b.js
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:
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):
// 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:
A launches B → B launches A → A 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:
npm install madge -g
madge src/It shows which files form circular dependencies.
Quick summary
| Term | Meaning |
|---|---|
| Circular dependency | Modules import each other (directly or through a chain) |
| Cause | A module requires another one that hasn't finished running yet |
| Consequence | undefined or a partially loaded module comes back |
| Fixes | Splitting 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
undefinedonrequire().
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.