behaviour of circular dependencies in resolving node.js modules

·

2 min read

I was reading the Node.js design patterns book when I came across this section of cycles in module resolution. This is the sample code:

Module a.js:

exports.loaded = false;
var b = require('./b');
module.exports = {
    bWasLoaded: b.loaded,
    loaded: true
};

Module b.js:

exports.loaded = false;
var a = require('./a');
module.exports = {
    aWasLoaded: a.loaded,
    loaded: true
};

Module main.js:

var a = require('./a');
var b = require('./b');
console.log(a);
console.log(b);

To summarise, main.js requires 2 modules, a.js and b.js. they internally require each other.

We know that require is a synchronous function, it has to run to completion but if it has cycles in it, how does it work? and if it doesn't whats the resolution steps like?

Here is the output if you run main.js:

{ bWasLoaded: true, loaded: true }
{ aWasLoaded: false, loaded: true }

These are CommonJS modules, and we can see the usage of exports and module.exports in the code. If you are not sure about these, read up this stack overflow answer: https://stackoverflow.com/questions/7137397/module-exports-vs-exports-in-node-js

based on the output, here is my assumption:

  1. bWasLoaded is true which mean the require to b was successful and it reached the point where we set loaded to true via module.exports.

  2. aWasLoaded is false, so does that mean that a did not load fully? in this case how is the exports variable exposed? loaded is true because the require call for b was completed.

So here is what is happening:

  1. main calls require('./a').

  2. entry created in require.cache for a.js. as part of the module load, loaded is set to false initially.

  3. b.js is required from a.js. No entry present for b so its added to require.cache. loaded is set to false initially.

  4. a.js is required from b.js. cache entry present for a.js so that is returned. Mind that a.loaded is still false. Which is why the return value of module b is { aWasLoaded: false, loaded: true }. which also means that the exports variable for b was updated.

  5. control comes back to step 3. the return value for a.js becomes { bWasLoaded: true, loaded: true }.

  6. main calls require('./b'). Since the module is already cached, it does not reload the module and aWasLoaded always remains false.

This is a good primer on how module.exports, module caching works in nodejs specifically for CommonJS modules.