DEV Community

Awaji-mitop N. Gilbert
Awaji-mitop N. Gilbert

Posted on

Circular dependency in Node.js and Nest.js

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
}

Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode

index.js is as follows:

const { utilA } = require('./A')

utilA()

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
  }
}

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Notice it correctly detects we have a circular dependency.

Found 1 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
}
Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode

Run madge with npm run find-circular-deps and notice the circular dependency is now gone.

Madge no circular dependency

Run the script index.js with node index.js and notice it now runs as expected to completion with no errors.

node index.js runs successfully

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 {}

Enter fullscreen mode Exit fullscreen mode

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());
  }
}

Enter fullscreen mode Exit fullscreen mode

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.

Circular dependency error in 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());
  }
}

Enter fullscreen mode Exit fullscreen mode

Refactor successful app

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());
  }
}
Enter fullscreen mode Exit fullscreen mode

Run the app and notice it works fine.
forwardRef successful app

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)

Collapse
 
chukwutosin_ profile image
Tosin Moronfolu

Great read, loved the simplicity in your explanation.

Collapse
 
successgilli profile image
Awaji-mitop N. Gilbert • Edited

Glad you find it so. Thanks

Collapse
 
davel_x profile image
davel_x

There is also an eslint rule that detects dependency cycles automatically :
github.com/import-js/eslint-plugin...
It's very useful.

Collapse
 
successgilli profile image
Awaji-mitop N. Gilbert • Edited

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.

Image description

Collapse
 
mayomi1 profile image
Mayomi Ayandiran

Best explanation on circular dependency!

Collapse
 
successgilli profile image
Awaji-mitop N. Gilbert

Thank you!