Circular dependency is a situation that arises when modules depend directly or indirectly on one another in a program.
In Nest.js, with dependency injection (DI), it can be easily identified as the affected module would not be created during application startup until this dependency issue is fixed. However, if this is a Node.js circular dependency, typical of incomplete module loading somewhere else in the application not using DI, it may go unnoticed resulting in runtime errors.
In this article, we will create, discuss and fix circular dependency in Node.js and Nest.js.
Circular dependency in Node.js
This is a case where 2 or more modules depend on each other by sharing program elements. For example, a module A
imports from B
and at same time, B
imports from A
.
We can summarise the dependency like so (A
-> B
-> A
).
The problem here, based on how module loading works is that Node.js while loading A
will find B
and while loading B
, will discover B
requires again module A
. In other to avoid an infinite loop, Node.js will return an incompletely loaded module A
in B
which will result in undefined imports in module B
.
The weird undefined
As pointed out earlier, incomplete module loading can result in unexpected errors during runtime as certain imports will be undefined. To drive this point, we will create a small Node example script to show how this can arise, how to troubleshoot it and refactor to verify our solution.
Consider a small Node.js project containing 3 files, A.js
, B.js
and index.js
where A.js
depends on B.js
and vice versa.
Say the content of A.js
is as follows:
const { utilB } = require('./B')
const nameObject = {
name: 'whatever'
}
const utilA = () => {
console.log('inside A')
console.log(`name is '${nameObject.name}' in A`)
utilB()
}
module.exports = {
utilA,
nameObject
}
B.js
is as follows:
const { nameObject } = require('./A')
const utilB = () => {
console.log('inside B')
console.log(`name is '${nameObject.name}' in B`)
}
module.exports = {
utilB,
}
index.js
is as follows:
const { utilA } = require('./A')
utilA()
As we can see, A.js
depends on B.js
and B.js
depends on A.js
. As a result of this, imports from A.js
in B.js
will be undefined as A.js
is not fully loaded at this time.
Run node index.js
in this project directory and observe that the output in the terminal throws an error Cannot read properties of undefined
which we know for a fact is defined. This is simply because of an incompletely loaded module A.js
in B.js
.
profg@AwajimitopsMBP circular-node (main) $ node index.js
inside A
name is 'whatever' in A
inside B
/Users/profg/Desktop/github/circular-deps/circular-node/B.js:5
console.log(`name is '${nameObject.name}' in B`)
^
TypeError: Cannot read properties of undefined (reading 'name')
at utilB (/Users/profg/Desktop/github/circular-deps/circular-node/B.js:5:37)
at utilA (/Users/profg/Desktop/github/circular-deps/circular-node/A.js:10:5)
at Object.<anonymous> (/Users/profg/Desktop/github/circular-deps/circular-node/index.js:3:1)
at Module._compile (node:internal/modules/cjs/loader:1105:14)
at Object.Module._extensions..js (node:internal/modules/cjs/loader:1159:10)
at Module.load (node:internal/modules/cjs/loader:981:32)
at Function.Module._load (node:internal/modules/cjs/loader:822:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12)
at node:internal/main/run_main_module:17:47
Troubleshoot circular dependency with Madge
Identifying this in a real project may be hectic and that is where madge comes in.
Madge is a tool for identifying circular dependencies in Node.js projects. I use it in my Nest.js projects too.
To do this, we need to install it as a development dependency and create a script for it.
Add madge to the project
npm install madge -D
My package.json looks like this now
{
"name": "circular-node",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"find-circular-deps": "madge . -c"
},
"author": "",
"license": "ISC",
"devDependencies": {
"madge": "^6.0.0"
}
}
Now, check for circular dependencies with madge by running the script find-circular-deps
in the package.json file. Note that madge . -c
simply tells madge we are checking in this same directory. emphasis on the dot .
used.
npm run find-circular-deps
Notice it correctly detects we have a circular dependency.
Avoid circular dependency in Node.js by refactor
We will fix this issue by a simple refactor. The refactor will depend on your project. However, for this dummy example used above, we can solve this by combining A.js
and B.js
into one file or move their relationship to a new file from where they can independently import their dependency. I will go with the latter here for sake of demonstration. Any option works.
We will create a new module C.js
and move into it the object nameObject
and refactor A.js
and B.js
to use C.js
thus, breaking the circular dependency.
C.js
will look like so
const nameObject = {
name: 'whatever'
}
module.exports = {
nameObject
}
A.js
will now look like so
const { utilB } = require('./B')
const { nameObject } = require('./C')
const utilA = () => {
console.log('inside A')
console.log(`name is '${nameObject.name}' in A`)
utilB()
}
module.exports = {
utilA,
}
B.js
will now look like so
const { nameObject } = require('./C')
const utilB = () => {
console.log('inside B')
console.log(`name is '${nameObject.name}' in B`)
}
module.exports = {
utilB,
}
Run madge with npm run find-circular-deps
and notice the circular dependency is now gone.
Run the script index.js
with node index.js
and notice it now runs as expected to completion with no errors.
Circular dependency in Nest.js
Circular dependency in Nest.js occurs when two classes depend on each other. This can be easily identified as Nest.js will throw an error and will only build after you resolve the circular dependency.
To show this, we need to quickly bootstrap a nest application and create 2 module classes that depend on each other. I will be leveraging the Nest.js cli tool to quickly bootstrap one and modify to achieve this.
Note that though we chose to demonstrate this with module classes, same can be applied in exactly same way with service/provider classes.
The Nest.js circular dependency problem
For this example, we will consider 2 module classes with names AppModule
and FileModule
which depends on each other.
The app.module.ts
module file
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { FileModule } from './file.module';
@Module({
imports: [FileModule],
controllers: [AppController],
providers: [AppService],
exports: [AppService],
})
export class AppModule {}
The file.module.ts
module file
import { Module } from '@nestjs/common';
import { AppController as FileController } from './app.controller';
import { AppService } from './app.service';
import { FileService } from './file.service';
import { AppModule } from './app.module';
@Module({
imports: [AppModule],
controllers: [FileController],
providers: [FileService],
})
export class FileModule {
constructor(
private readonly appService: AppService,
) {}
onApplicationBootstrap() {
console.log(this.appService.getHello());
}
}
What we have done above is to forcefully create a circular dependency by importing also the AppModule in the FileModule just to access our appService. On attempt to start the app, you should get this error in the terminal.
Fix Nest.js circular dependency by refactor
A refactor most times, is what we need to get around this issues.
In other cases, one may need to create a new module to hold the shared dependencies and have that module imported in our 2 new modules to break the circular dependency but it depends on what one intends to achieve and the project architecture at hand. However, in this simple example, we created the problem and so it is easy to fix as we know all we need to do is not import the entire AppModule
module and just inject our appService
as a provider in FileModule
.
Refactor file.module.ts
like so
import { Module } from '@nestjs/common';
import { AppController as FileController } from './app.controller';
import { AppService } from './app.service';
import { FileService } from './file.service';
@Module({
controllers: [FileController],
providers: [FileService, AppService],
})
export class FileModule {
constructor(
private readonly appService: AppService,
) {}
onApplicationBootstrap() {
console.log(this.appService.getHello());
}
}
Fix Nest.js circular dependency with forwardRef
When a refactor is not an option for your use case, then Nest.js forwardRef
utility function can be used to overcome this issue.
Back to our original failing FileModule
version that imports the entire AppModule
, we just need to use forwardRef
with the import of AppModule
.
The file file.module.ts
will now look like so
import { Module, forwardRef } from '@nestjs/common';
import { AppController as FileController } from './app.controller';
import { AppService } from './app.service';
import { FileService } from './file.service';
import { AppModule } from './app.module';
@Module({
imports: [forwardRef(() => AppModule)],
controllers: [FileController],
providers: [FileService],
})
export class FileModule {
constructor(
private readonly appService: AppService,
) {}
onApplicationBootstrap() {
console.log(this.appService.getHello());
}
}
Run the app and notice it works fine.
Conclusion:
Circular dependency in Nest.js and Node.js are simply the same things. However, Nest.js detects this during dependency injection and prevents it by throwing a circular dependency error. The main problem is just an incompletely loaded module which can still happen in other parts of a Nest.js application not using dependency injection and go unnoticed until something begins to break.
We can find these issues using the npm package madge and fix them by a simple refactor. Where a refactor is not an option, we can always use the Nest.js utility function forwardRef
which works for both modules and providers.
The code used in this article can be found on github. The initial circular dependency code problems are in the branch circular-dependency-problem
Top comments (6)
Great read, loved the simplicity in your explanation.
Glad you find it so. Thanks
There is also an eslint rule that detects dependency cycles automatically :
github.com/import-js/eslint-plugin...
It's very useful.
Thanks, this is useful too.
However it is worth noting this about this eslint rule.
This rule is comparatively computationally expensive. If you are pressed for lint time, or don't think you have an issue with dependency cycles, you may not want this rule enabled.
Best explanation on circular dependency!
Thank you!