I am using the Inquirer library with Node.js and I still get the pyramid of doom when using promises, what am I doing wrong?
Just FYI the inquirer library API is basically:
inquirer.prompt([
question1,
question2,
question3,
...
questionX
]).then(function(answers){});
where answers is a hash, with keys that represent each question. Nothing really out of the ordinary here.
Anyway, using the API, I always get getAnswersToPrompts().then(function(answers){})
and it seems more convenient to keep nesting the promises inside the previous one...like so:
function run (rootDir) {
return watchHelper().then(function (answers) {
return chooseDirs({
allowDirs: answers.allow,
originalRootDir: rootDir,
onlyOneFile: false
}).then(function (pathsToRun) {
assert(pathsToRun.length > 0, ' You need to select at least one path.');
return getOptions(availableOptionsForPlainNode).then(function (answers) {
const selectedOpts = answers[ 'mand-line-options' ];
return localOrGlobal().then(function (answers) {
const sumanExec = answers.localOrGlobal;
console.log(' => ', colors.magenta.bold([ '$', sumanExec, '--watch', pathsToRun, selectedOpts ].join(' ')));
});
});
});
}).catch(rejectionHandler);
}
I could possibly do this instead:
function run(){
return makePromise()
.then(fn1(data1))
.then(fn2(data2))
.then(fn3(data3))
}
where fn1,fn2,fn3 look like:
function fnX(data){
return function(answers){
return promise(data);
}
}
but this just makes things more plicated to understand AFAICT
Just be as clear as possible, I definitely need the result of the previous promise, but sometimes I need the result from the promise prior to that or even the result prior to that.
Nesting the functions allows the data I need to be in scope, thanks to closures etc.
I am using the Inquirer library with Node.js and I still get the pyramid of doom when using promises, what am I doing wrong?
Just FYI the inquirer library API is basically:
inquirer.prompt([
question1,
question2,
question3,
...
questionX
]).then(function(answers){});
where answers is a hash, with keys that represent each question. Nothing really out of the ordinary here.
Anyway, using the API, I always get getAnswersToPrompts().then(function(answers){})
and it seems more convenient to keep nesting the promises inside the previous one...like so:
function run (rootDir) {
return watchHelper().then(function (answers) {
return chooseDirs({
allowDirs: answers.allow,
originalRootDir: rootDir,
onlyOneFile: false
}).then(function (pathsToRun) {
assert(pathsToRun.length > 0, ' You need to select at least one path.');
return getOptions(availableOptionsForPlainNode).then(function (answers) {
const selectedOpts = answers[ 'mand-line-options' ];
return localOrGlobal().then(function (answers) {
const sumanExec = answers.localOrGlobal;
console.log(' => ', colors.magenta.bold([ '$', sumanExec, '--watch', pathsToRun, selectedOpts ].join(' ')));
});
});
});
}).catch(rejectionHandler);
}
I could possibly do this instead:
function run(){
return makePromise()
.then(fn1(data1))
.then(fn2(data2))
.then(fn3(data3))
}
where fn1,fn2,fn3 look like:
function fnX(data){
return function(answers){
return promise(data);
}
}
but this just makes things more plicated to understand AFAICT
Just be as clear as possible, I definitely need the result of the previous promise, but sometimes I need the result from the promise prior to that or even the result prior to that.
Nesting the functions allows the data I need to be in scope, thanks to closures etc.
Share Improve this question edited Nov 13, 2016 at 2:24 Alexander Mills asked Nov 13, 2016 at 2:09 Alexander MillsAlexander Mills 101k166 gold badges537 silver badges918 bronze badges 7- 4 an upvote for using the term "pyramid of doom" – imjared Commented Nov 13, 2016 at 2:12
- LOL I don't really mind nested calls, but just curious if there is a way around it – Alexander Mills Commented Nov 13, 2016 at 2:13
-
1
Have you tried
async
module with itsasync.waterfall
? This would be more cleaner in your case. – Aruna Commented Nov 13, 2016 at 2:14 - 1 Don't call then directly on getOptions and localOrGlobal. You can continue in the same level of the chain all the way through (no nesting) as far as I can tell. – cYrixmorten Commented Nov 13, 2016 at 2:36
- 1 Sorry didn't answer the last part. It is pletely allowed to have a set of variables before the promise chain that are set during the chain. So you can easily use variables that were set in the first part later on. – cYrixmorten Commented Nov 13, 2016 at 2:40
2 Answers
Reset to default 8Return the next Promise before calling then
:
function run (rootDir) {
var pathsToRun;
return watchHelper()
.then(function (watchHelperAnswers) {
return chooseDirs({
allowDirs: watchHelperAnswers.allow,
originalRootDir: rootDir,
onlyOneFile: false
});
}).then(function (chooseDirsResult) {
assert(chooseDirsResult.length > 0, ' You need to select at least one path.');
pathsToRun = chooseDirsResult;
return getOptions(availableOptionsForPlainNode);
}).then(function (getOptionsAnswers) {
const selectedOpts = getOptionsAnswers[ 'mand-line-options' ];
return localOrGlobal();
}).then(function (localOrGlobalAnswers) {
const sumanExec = localOrGlobalAnswers.localOrGlobal;
console.log(' => ', colors.magenta.bold([ '$', sumanExec, '--watch', pathsToRun,
selectedOpts ].join(' ')));
}).catch(rejectionHandler);
}
but sometimes I need the result from the promise prior to that or even the result prior to that
The only instance of this in your example is pathsToRun
. I think nesting functions two or three deep to acmodate this is still readable, but your other option is to define a variable outside the promise chain, which I have shown above for pathsToRun
.
Lastly, your example uses three different variables all called answers
throughout the promise chain, which might be adding to the confusion. In general I think it is fine to use the same name for promise callback results, but I have renamed them here for clarity in this answer.
@Joe Daley's answer is near perfect, but to add one more thing. I really don't like that side assignment to variables at the top of the function. I never liked it with async.waterfall/async.series...and I don't like it with promises either...the following pattern should avoid that. We accumulate the data in each promise callback, and then in the final promise callback we have all the data.
//start
function run (rootDir) {
return watchHelper()
.then(watchHelperAnswers => {
return chooseDirs({
allowDirs: watchHelperAnswers.allow,
originalRootDir: rootDir,
onlyOneFile: false
});
})
.then(chooseDirsResult => {
return getOptions(availableOptions).then((options) => {
return { //accumulate answers
options: options,
pathsToRun: chooseDirsResult
}
});
})
.then((obj) => {
return localOrGlobal().then(answers => {
return Object.assign(obj,{ //accumulate answers
localOrGlobal: answers.localOrGlobal
});
});
}).then((obj) => {
const {...allTheAnswers} = obj;
}).catch(rejectionHandler);
}
boom! Now you can avoid that awkward assignment to variables at the top. If you don't see how this works...ask me.