JavaScript modules and cyclic dependencies

Circular dependencies between modules are generally a bad thing. They make two modules tightly coupled and there is a very high chance that if you change one of them, that’s going to affect the other. You effectively make two possibly independent modules look like a single unit of code, defeating one of the main purposes of modularisation.

Having said that, despite your good will, you might find yourself in a situation where you need to handle a circular dependency. Depending on which module loader or strategy you are using, adding a circular dependency might work out of the box. Besides, learning how they are handled will give you insights on how your module loader works under the hood.

ES5 – AMD with RequireJS

With RequireJS you need to make some adjustments, as explained in their dedicated API docs chapter:

You can declare the require function as a dependency and use it to fetch the module that you want to load later, rather than as a normal dependency.

Let’s create two modules that depend on each other:

The first module exposes a couple of functions which delegate the results to the second module and a “name” string, which creates the backward dependency. The latter uses the string exposed by moduleA.

If you import moduleA and print the result of getOtherModuleName it will work, because both modules are loaded at this time. What you cannot do is moduleB.getThisName() because at the time of loading, the moduleA dependency was undefined and it will throw a TypeError.

You can instead load moduleA manually using the require dependency:

This is basically moving the dependency on moduleA from load time to run time.

ES5 – CommonJS

Let’s do something similar creating three NodeJS modules:

With this scenario the result is similar to the previous AMD example:

This is because, when module B starts, it will initially get an empty object from the module A import, which is nothing but the initial value of the exports object in A. Once A has done loading, it will have exported its own two function overriding that initial reference with a new object literal; that empty object is now disconnected from A. B will try to get the name property from an empty object.

We can fix it by exporting the functions separately without overriding the exports object:

If you run it now:

The variable a in module B will now be properly populated with the exported functions and name once module A finishes loading.

Now let’s play with it a bit and change module A to look like this:

Bear in mind that exports and module.exports initially point to the same object. If you want to know more about the relationship between the two, have a look at

Now, if you add a

to module C, you will get a possibly unexpected result:

This happens because module C is getting the object literal straight away from module A, which completes the loading before the import, while module B is getting the name property from the initial exports object. If that sounds confusing, the best thing you can do is start playing with it a bit or look at the require function source code to understand what’s happening under the hood.

ES 6

With the new ECMAScript standard, a completely different approach has been chosen, which makes circular dependency work without any workaround. The only case in which they won’t work, but that’s valid for the above examples also, is when you try to use the dependency in the module body rather then in a function for later use.

This will work as expected, like the second CommonJS example. Now, I would love to should you how to run it, but there is no native support for ES6 modules in browsers nor in NodeJS. What you can do is use transpilers like Babel; you’ll see that the code will be translated to something similar to what we had in the second CommonJS example.

The peculiarity of ES6 exports is that they are live and readonly views on the exported values. Take for example the string “moduleA”: you might think that the export happens by value, in which case any subsequent change to the name variable would hold no effect on the exported value; that’s true for CommonJS, but not for ES6 modules.

Let’s make an addition to our Module A:

We exported another function that updates the name variable. As I said there is no way to see what happens natively, but we can take a look at how Babel interprets it:

This is the Babel output in its “es2015” mode; take a look at what happens now inside the updateName function. It’s not only updating the local variable name, but also the property of the exported object. We saw earlier that exports references the object that will be returned by require, that means that modules depending on module A will see the new name as well.