Skip to main content

Command Palette

Search for a command to run...

Understanding Circular Dependency Errors in Node.js

Updated
4 min readView as Markdown
Understanding Circular Dependency Errors in Node.js

Recently, I faced an unfamiliar error while modifying backend code in NodeJS where I was importing a utility function in service file, that particular function is showing error TypeError: —function is not defined whenever the service function using the imported utility is being invoked, but in any other module that utility was working perfectly fine. After few minutes of debugging on my own, I quickly described this situation to chatgpt, and it replied within seconds with troubleshooting steps, one by one I followed the steps from which I found out that I was facing a error known as circular dependency error.

what is causing this error, the file defining the utility function was also importing a function from the same service file, which is using the utility.

So I fixed it by removing the controller function using service file function from the utility file and placing it in the controller file.

short example illustration

utils/index.js
const exampleService = require('./services/users.service.js')

function exampleUtil(){
----- code ----
}

async function exampleController(){
await exampleService.getUsers()
} 

module.exports = {
exampleUtil,
exampleController
}

services/users.service.js

const {exampleUtil} = require('./utils')

function getUsers(){
---code--
exampleUtil()
}

In this example illustration, users.service.js file requires index.js file and index.js file requires users.service.js which was causing circular dependency error and giving error in the console as

TypeError: exampleUtil is not defined

Below is the detailed explanation of this topic

In Node.js, modularization is a key practice to structure applications, allowing developers to separate concerns and organize code into different files or modules. However, as your application grows, you might run into an issue called a circular dependency. This can lead to confusing bugs, incomplete module exports, or runtime errors that are difficult to debug.

What is a Circular Dependency?

A circular dependency occurs when two or more modules depend on each other either directly or indirectly, forming a loop in the dependency graph.

Example of Direct Circular Dependency:

// fileA.js
const b = require('./fileB');
module.exports = functionA() {
  console.log("Function A");
  b();
}

// fileB.js
const a = require('./fileA');
module.exports = functionB() {
  console.log("Function B");
  a();
}

In this example:

  • fileA requires fileB

  • fileB requires fileA
    \=> Circular dependency!

When Node.js encounters this, it partially loads one of the modules, which may cause undefined exports or runtime errors.

How Node.js Handles Circular Dependencies

Node.js uses the CommonJS module system, which caches modules after the first time they are required. During a circular import:

  1. Module A is loaded.

  2. Module A requires Module B.

  3. Module B starts loading but requires Module A again.

  4. Since Module A is already in the process of being loaded, Node returns a partially complete export object for A to B.

  5. B uses this incomplete export, potentially leading to bugs.

This mechanism prevents infinite loops, but results in unpredictable behavior if your modules rely on each other during initialization.


Symptoms of Circular Dependencies

  • TypeError: foo is not a function

  • Exported values being undefined or incomplete

  • Unexpected behavior or application crashes at runtime

How to Detect Circular Dependencies

1. Manually Inspect the Require Chain

Look at the require statements and see if any files form a cycle.

2. Use Tools

You can use static analysis tools like:

  • madge: Visualize and detect circular dependencies in your codebase.

  • npx madge --circular ./src

How to Fix or Avoid Circular Dependencies

1. Refactor Shared Logic to a New Module

Extract the shared functionality into a third file to break the circular chain.

// shared.js
function sharedLogic() {
  console.log("Shared logic");
}
module.exports = sharedLogic;

// fileA.js
const shared = require('./shared');
module.exports = () => {
  console.log("A uses shared");
  shared();
}

// fileB.js
const shared = require('./shared');
module.exports = () => {
  console.log("B uses shared");
  shared();
}

2. Use Lazy require (inside functions)

Defer the import until the function is called to avoid circular access during initialization.

// fileA.js
module.exports = () => {
  const b = require('./fileB'); // Lazy load
  console.log("A calls B");
  b();
}

3. Rethink Module Responsibilities

If modules are tightly coupled, consider whether they should be merged or if the design can be improved to reduce interdependencies.

4. Use Dependency Injection

Instead of requiring modules directly, pass them as parameters to functions or constructors. This decouples modules from each other.

// fileA.js
module.exports = functionA = (b) => {
  console.log("Function A");
  b();
}

Conclusion

Circular dependencies in Node.js can cause serious headaches, especially in large applications. While Node.js handles them to some extent by returning partial exports, relying on this behavior leads to fragile and hard-to-maintain code. Always aim to structure your modules to avoid cycles, and use tools like madge to audit your project.

By applying good design principles, lazy loading, and modular decoupling strategies, you can build more maintainable and robust Node.js applications.