10

I'm trying to rewrite a module to return a different value than before, but now it uses an async call to get that value. (with child_process if it matters). I've wrapped it in a Promise, but that's not critical to me - it can be in the original child_process callback, but the problem is I can't chain the promise to everywhere in the app because I need this to become synchronous. Here's my module:

const { exec } = require("child_process");

const platformHome = process.env[process.platform === "win32" ? "USERPROFILE" : "HOME"];

const getExecPath = new Promise((resolve, reject) => {
  const path = process.env.GEM_HOME;

  if (path) {
    resolve(path);
    return;
  }
  exec("gem environment", (err, stdout, err) => {
    if (err) {
      reject(err);
    }
    const line = stdout.split(/\r?\n/).find(l => ~l.indexOf("EXECUTABLE DIRECTORY"));
    if (line) {
      resolve(line.substring(line.indexOf(": ") + 2));
    } else {
      reject(undefined);
    }
  });
});

let GEM_HOME = undefined;

getExecPath
  .then(path => (GEM_HOME = path))
  .catch(() => (GEM_HOME = `${platformHome}/.gem/ruby/2.3.0`))
  .then(
    () => (module.exports = GEM_HOME) // or simply return it
  );

Clearly, when requiring the module, this doesn't work - and if I return the promise itself, and use then after require - my next module.exports will be async, and this chain will carry on. How do I avoid this pattern?

casraf
  • 21,085
  • 9
  • 56
  • 91

2 Answers2

15

Modules in Node that you load with require() are loaded synchronously and it is not possible for require to return any value that is loaded asynchronously. It can return a promise but then users of that module would have to use it as:

require('module-name').then(value => {
  // you have your value here
});

It would not be possible to write:

var value = require('module-name');
// you cannot have your value here because this line
// will get evaluated before that value is available

Of course you can have the promise resolved inside of your module and make it set a property on the exported object by adding something like this:

module.exports = { GEM_HOME: null };

and changing:

module.exports = GEM_HOME

to:

module.exports.GEM_HOME = GEM_HOME

In that case, every other module that uses this module as:

var x = require('module-name');

will have x.GEM_HOME originally set to null but it would eventually get changed to a correct value some time later. It would not be available right away though, because require() returns before the promise is settled and the value is set.

There is an ongoing discussion to introduce asynchronous module loading with different syntax and semantics that may be suited for your use case. It's a controversial subjects and it's worth reading all of the rationale behind it - see:

See also this answer for more details:

rsp
  • 107,747
  • 29
  • 201
  • 177
  • Great answer, albeit a disappointing one. I ended up finding out that in my specific case, I can use `child_process.execSync`, which solves my problem but doesn't address the real issue with async requirements. – casraf Dec 26 '16 at 10:10
  • x.GEM_HOME is always undefined. It seems like a copy of the object is exported. – Avery235 Nov 19 '17 at 16:38
0

Node.js modules are loaded synchronously.

You can deal with this exporting the Promise value.

#@ your module.js
module.exports = new Promise()

and:

#@ your app.js
const mod = require('./module');

mod.then((value => ...);
Dario
  • 3,905
  • 2
  • 13
  • 27
  • 2
    Sure, but in the second block - that means if my exported module depends on that value, it ALSO has to be done async, and so is the next, and the next... that's what I'm trying to avoid – casraf Dec 22 '16 at 10:13