Handling Circular Dependencies in Nodejs

Zachary Keeton
4 min readOct 27, 2020

If you’ve tried to require a module and received {} when you should have a fuller object there, you probably have a circular dependency issue where, for example, moduleA requires moduleB that requires moduleC that requires moduleA.

Circular dependencies are not necessarily problems in Nodejs. In fact, you can use them at-will without any nasty surprises, so long as you understand these 5 things about how Nodejs and require actually works.

1. require("./fileA") only gives you fileA's module.exports object

This first one is the most basic. Take a file named fileA.js that has some content and an exports object like so:

// fileA.js
const name = "bob"
const age = 75
module.exports = { weight: 200 }

If you require it in another file, say index.js, you will only have the module.exports object, not the rest of the content ( name or age).

proof:

// index.js
const fileA = require("./fileA")
console.log(fileA)
// { weight: 200 }

2. If you require the same thing in multiple places you get the same object.

If you’ve spent any time learning Nodejs, you know that each time you require a module, you actually get the same reference to that module's exports object each time, not a separate instance. E.g. if fileA.js and fileB.js both require("./fileC"), they'll each get the same reference to fileC's module.exports object.

We an prove this like so:

// fileC.js
module.exports = { name: "zach" }
// fileA.js
// require fileC and export that reference for comparison
module.exports = { cExportsReference: require("./fileC") }
// fileB.js
// require fileC and export that reference for comparison
module.exports = { cExportsReference: require("./fileC") }
// index.js
// require fileA and file B and compare their fileC exports reference.
const fileAExports = require("./fileA")
const fileBExports = require("./fileB")

console.log(fileAExports.cExportsReference === fileBExports.cExportsReference)
# run it
$ node index.js
true # fileA and fileB both had the same reference to fileC's exports object.
$

3. The exports objects are held in require.cache

Every file in Nodejs is considered a module. When these files are opened, there is a cache entry started in require.cache for that module which includes the module.exports object that is built as the module is loaded. This is the object reference given to each other file that require s the module. To see the cache at any time, simply print it:

// index.js
console.log(require.cache)
// an array of modules, their state, and their exports objects

and you will see all loaded modules, including the current file, and their respective exports objects.

4. When you require a module, you are given that module's exports object reference from the cache at that point in time.

Whenever you are in a file, say fileA, and you require("./fileB"), you are simply given that modules exports reference from the cache at that point in time.

e.g.

// fileA.js
// immediately creates a cache entry for fileA with an exports obj: {}

// here - module.exports = {}
module.exports.name = "Zach"
module.exports.country = "USA"

// here - module.exports = { name: "Zach", country: "USA" }
// Now, jump over to fileB and get the exports obj
const fileBexports = require("./fileB")

// this will not be available in fileB above yet.
module.exports.planet = "Earth"
// fileB.js

// Require fileA (circular reference).
const fileAexports = require("./fileA")
// fileA exports at this point in time from require.cache: { name: "Zach", country: "USA" }

console.log(fileAexports.planet) // undefined (since the planet property hasn't been added yet up in fileA)

5. You can have circular references so long as what you need has been added to the exports object by the time you need it.

So long as what you need from a required module has been loaded into its module.exports object by the time you use it, you won't have a bad time.

However, developers often get into circular reference trouble while using the revealing module pattern.

In the file below, we first require fileB which really means, "get a reference to fileB's exports object".

// fileA
// module.exports is {}

const fileBexports = require("./fileB") // jump to fileB and get `exports` obj.
console.log(fileBexports.country) // USA

const name = "Zach"
const planet = "Earth"

// here - module.exports is {}
module.exports = { name, planet }
// only now - module.exports is { name: "Zach", planet: "Earth" }

At the point when we require file B, file A’s export object reference in the require.cache is still {}.

// fileB

// get a reference to fileA's exports object (circular reference)
const fileAexports = require("./fileA") // at this moment. fileA.exports = {}

// BUG: here we must have the name, but it is undefined. So we would scratch our heads and start debugging.
// But now we know why this happens: `.name` has not been added yet to fileA's module.exports object in require.cache at this point in time.
console.log(fileAexports.name) // undefined

module.exports = { country: "USA" }
$ node fileA.js
undefined
USA
$

By ensuring that what we need is in the exports object by the time we need it, we can handle circular references.

Here’s one not-so-elegant way to fix it:

// fileA
module.exports.name = "Zach" // add to the export objects before fileB needs it.

const fileBexports = require("./fileB") // jump to fileB and get `exports` obj.

console.log(fileBexports.country) // USA

const planet = "Earth"
module.exports = { name, planet }

Now, at that point when we require file B, file A’s export object reference in the require.cache DOES include a name: "Zach" property.

// fileB

// get a reference to fileA's exports object (circular reference)
const fileAexports = require("./fileA") // at this moment. fileA.exports = {name: "Zach"}

console.log(fileAexports.name) // Zach

module.exports = { country: "USA" }
$ node fileA.js
Zach
USA
$

Now, you have a circular reference, but all is still well with the world. I leave it to you to make the fix more elegant. And don’t listen to those who would say, “just don’t do that” ahem stackOverflow ahem. That may be good advice for junior developers that will get themselves in trouble, but circular dependencies are OK in Nodejs and are a good addition to a senior developer’s bag of tricks.

Happy coding.

Originally published at https://zacharykeeton.gitlab.io.

--

--

Zachary Keeton

A 15th-year Web Dev/Engineering Manager. Formerly building products and leading teams at Plus One Robotics in San Antonio, Texas, USA