Dave Normington

Software Engineer

Circular Dependencies

Feb 2, 2019

Circular or recursive dependencies are modules that eventually depend upon themselves. This causes unexpected errors at run time such as module exports seemingly being undefined.

An example

Consider the following code

// a.js
const { b } = require("./b");
function a() {
b();
}
module.exports.a = a;
a();

// b.js
const { a } = require("./a");
function b() {
a();
}
module.exports.b = b;

We have 2 modules, a.js and b.js. a depends on b and b depends on a - a circular dependency!

Let’s execute the code to see what happens:

$ node ./a
/Users/davidnormo/tmp/b.js:4
a()
^

TypeError: a is not a function
at b (/Users/davidnormo/tmp/b.js:4:3)
at a (/Users/davidnormo/tmp/a.js:4:2)
at Object.<anonymous> (/Users/davidnormo/tmp/a.js:7:1)
...

What happened? We got a TypeError which means that the error occured at runtime. The code was compiled by the JS engine (V8 in our case) so the circular dependency wasn’t caught early. This is because dependencies in Node are resolved at runtime, require is just another function.

When it came to executing the code, the stack trace tells us the story. Reading from the bottom upwards (I’ve cut out the start of the stack that references node internals):

So if a is not a function when b tries to call it, what is it? It’s undefined.

I don’t know exactly how the internal node module resolution mechanism works - it’s something I want to do a bit more research on - but this is one of the tricky cases.

The above example is obviously contrived to illustrate the point. The times I’ve found this out in the wild have had many more modules involves e.g.

a -requires-> b -requires-> c -requires-> d -requires-> a

These are a lot harder to work out what is going on. You have to try and keep the module dependency tree in your head or keep all the files open in your editor.

How to fix

In our simple example not only do the modules a.js and b.js depend on each other but the functions a and b call each other. This is a very tight coupling. If all the code was put in the same module the functions would call each other recursively until we get a “Maximum call stack size exceeded” error. In this case it is probably best to inline b into a.

In the case that the modules depend recursively but not the functions, you can extract out another module that doesn’t depend on one of the modules in the recursive loop. Or you can duplicate some code from one of the modules in the loop (a short term fix).

Round up

So next time you see strange errors where module dependencies are inexplicably causing an error, think, it may be caused by a circular dependency!