1

I'm writing a program with several threads and a lot of requires. I need circular dependencies otherwise there will be too many files.

I have a function that can prevent it from looping, however node complains about it before it can even run. I've tried looking for a way, but its hard to find a "bypass" instead of moving a lot of stuff around.

Is it possible to disable the protections and let it run anyway?

Here's my code;

constants.js

// This should prevent any circular dependency but node shuts it down too eagerly.
function _getCallerFile() {
    // Returns the file that originally called.
    
    var err = new Error();
    Error.prepareStackTrace = (_, stack) => stack;
    Error.prepareStackTrace = undefined;
    return err.stack.toString();
}

var x = _getCallerFile();
console.log(x);
console.log(x.includes(".js"))
console.log(x.includes("threading.js"))

//if (!x.includes("input.js")) var input = require('./constants/input.js');
if (!x.includes("threading.js")) var threading = require('./constants/threading.js');

module.exports ={ 
    threading: threading
    //input: input
};

threading.js

var x = require('../constants.js');

module.exports.init = async function (whoami) {
    x.sendMsg("debug", whoami, `Starting!`);
}

index.js

var x = require('./constants.js');

async function main() { 
  x.threading.init(whoami);
}
main();
  • 2
    No. It's not possible. It is not a protection. Node just does not support circular dependencies. – slebetman Nov 17 '21 at 01:17
  • @jfriend00 Node [supports circular dependencies just fine](https://stackoverflow.com/q/10869276/1048572), in both commonjs modules and ES6 modules. The trick is not use stuff before you defined it, as the order of execution can be hard to predict (depending on the entry point). – Bergi Nov 17 '21 at 02:01
  • "*node shuts it down too eagerly.*" - what error message are you getting? – Bergi Nov 17 '21 at 02:03
  • FYI, I'd strongly suggest you switch from using `var` to `const` and `let`. There is pretty much no reason to use `var` any more as it allows a number of poor coding practices that `const` and `let` are more strict about. – jfriend00 Nov 17 '21 at 04:00

2 Answers2

3

Node.JS Is it possible to prevent node from stopping execution of circular dependencies?

Nodejs cannot run with many types of circular dependencies. There are some ways you can live with certain things if you are really, really careful in how you write your code, what order you define things in and what order you reference things in. See the description here for details, but in general, it's much safer to just remove the circular dependency in its entirety rather than try to live with it or code around it.

Imagine the maintenance liability of your code if you very carefully create a circular dependency that actually works because of the careful timing of when you declare and access things and then an innocent looking modification to the code suddenly breaks things. It is generally safer to not have circular dependencies and a little refactoring into shared files can usually solve it easily and safely.

The usual solution here is to rework the layout of your code to avoid creating the circular dependency in the first place. It is always possible to break some common code into a shared file and avoid the circular dependency and you should be able to do this without creating a zillion files.

In some cases, it's simpler to combine two files into one file where you can have circular code dependencies between say two classes within the same file. If you show the real code with the real problem, we could advise on good options for avoiding the circular dependency.


In your specific example, you could work around it by delaying one of your require() statements. For example, if you change part of threading.js from this:

var x = require('../constants.js');

module.exports.init = async function (whoami) {
    x.sendMsg("debug", whoami, `Starting!`);
}

to this:

module.exports.init = async function (whoami) {
    var x = require('../constants.js');
    x.sendMsg("debug", whoami, `Starting!`);
}

Then, you no longer run into the circular dependency because constants.js is not loaded by threading.js until AFTER threading.js is already done being loaded. Or, another way of looking at it, is that threading.js loads without requiring constants.js, thus avoiding the circularity during load. Note, this won't translate as well to ESM modules which requires that dynamic (non-static) imports be asynchronous and use the function version of import() that returns a promise.

This will load constants.js upon the first call to .init() which isn't ideal, but once it is loaded the first time, it will come from the module cache every time after that.

I'd still personally suggest refactoring to avoid this, but there are some tricks with load timing that can avoid the circularity.

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • "*Nodejs just doesn't support circular dependencies*" - [the documentation](https://nodejs.org/api/modules.html#modules_cycles) suggests otherwise – Bergi Nov 17 '21 at 02:10
  • @Bergi - Answer modified accordingly. I still wouldn't recommend trying to "live" with a circular dependency as it's kind of ticking time bomb just waiting for the wrong edit to the code that breaks things. – jfriend00 Nov 17 '21 at 03:36
  • @RottenDirector - I've significantly updated my answer. – jfriend00 Nov 17 '21 at 03:57
  • I still don't think there's anything wrong with circular dependencies. Yes, you need to think about what you're doing, but as long as you never access the imported modules from top-level code (but only inside functions called later), and if you never overwrite `module.exports`, things cannot break. The correct solution for the OP's case is to change `module.exports = { threading };` into `exports.threading = threading` imo. – Bergi Nov 17 '21 at 11:02
  • @Bergi - Why don't you write your own answer then, if that's all you really think is required here. The caller's code seems incomplete because `threading` is being defined inside an `if` statement so it's unclear what happens if that `if` isn't triggered as it would currently generate an error with what they've shown. I find circular dependences "brittle". One small change can break everything which isn't what I'd generally consider that way I like to leave code for others to maintain. And, if a little code can be reorganized to avoid entirely, why isn't that a better way to write code? – jfriend00 Nov 17 '21 at 15:23
  • I had not written an answer yet because indeed the OPs code seems incomplete and they didn't come back to my comment on the exact error message, and I probably would've closed as a duplicate then. But yeah, of course it's better to refactor if a different code organisation is doable, but it's not necessarily a little change. I think of circular dependencies only as brittle if they depend on load order, which I would indeed avoid. – Bergi Nov 17 '21 at 15:42
0

Your maybe over complicating things here.

Looking at your code using the _getCallerFile hack to try and auto require looks like a problem waiting to happen. eg. What if you legitimately had two copies of threading.js.

Below is your code adjusted to work the way you want without having to implement any funky stuff.

constants.js

module.exports ={ 
   // threading: threading <- filled during bootstrap stage
};

threading.js

var x = require('../constants.js');

async function init(whoami) {
    x.sendMsg("debug", whoami, `Starting!`);
}

//bootstrap
x.threading = init;

module.exports = init;

index.js

require('./threading.js'); //bootstrap
var x = require('./constants.js');

async function main() { 
  x.threading.init(whoami);
}
main();

What's even better, doing it this way will work if you ever decided to go ES module loader, or you wanted start using TypeScript.

Keith
  • 22,005
  • 2
  • 27
  • 44