Catalin P
Catalin P

Reputation: 59

Promise nesting vs chaining style

New to promises; consider the case where there is promiseA() and promiseB(a) which depends on the first result, and I want to collect results from both and perform a third action doSomething(a, b):

Style A (closure/nesting)

promiseA().then(function (resultA) {
  return (promiseB(resultA).then(function (resultB) {
    doSomething(resultA, resultB);
  }));
});

Style B (return value/chaining)

promiseA().then(function (resultA) {
  return Promise.all([resultA, promiseB(resultA)]);
}).spread(function (resultA, resultB) {
  doSomething(resultA, resultB);
});

As far as I can tell, these are equivalent:

As a matter of style, Style B reduces indentation (pyramid of doom).

However, Style B is more difficult to refactor. If I need to introduce an intermediate promiseA2(a) and doSomething(a, a2, b), I need to modify 3 lines (Promise.all, spread, doSomething), which can lead to mistakes (accidental swapping etc), while with Style A I only to modify 1 line (doSomething) and the variable name makes it clear which result it is. In large projects, this may be significant.

Are there other non-functional trade-offs between the two styles? More/less memory allocation in one vs the other? More/fewer turns around the event loop? Better/worse stack traces on exceptions?

Upvotes: 1

Views: 1840

Answers (1)

trincot
trincot

Reputation: 350280

I think the non-functional trade-offs between the two methods are not so important: the second method has some overhead in creating the array, and spreading the corresponding results, and it will create one more promise. However in an asynchronous flow, all this is negligible in my opinion.

Your major concern seems to be the ease of refactoring.

For that I would suggest to use an array of functions, and reduce over it:

[promiseA, promiseB, doSomething].reduce( (prom, f) =>
    prom.then( (res = []) => ( f(...res) || prom).then( [].concat.bind(res) ) )
, Promise.resolve() );


// Sample functions
function wait(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

function promiseA() {
    console.log('promiseA()');
    return wait(500).then(_ => 13);
}

function promiseB(a) {
    console.log('promiseB(' + a + ')');
    return wait(500).then(_ => a + 2);
}

function doSomething(a, b) {
    console.log('doSomething(' + a + ',' + b + ')');
}

The idea is that the next function in the then callback chain gets all of the previous results passed as arguments. So if you want to inject a promise-returning function in the chain, it is a matter of inserting it in the array. Still, you would need to pay attention to the arguments that are passed along: they are cumulative in this solution, so that doSomething is not an exception to the rule.

If on the other hand, you only want doSomething to get all results, and only pass the most recent result to each of the intermediate functions, then the code would look like this:

[promiseA, promiseB].reduce( (prom, f) =>
    prom.then( (res = []) => f(...res.slice(-1)).then( [].concat.bind(res) ) )
, Promise.resolve() ).then(res => doSomething(...res));

function wait(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

function promiseA() {
    console.log('promiseA()');
    return wait(100).then(_ => 13);
}

function promiseB(a) {
    console.log('promiseB(' + a + ')');
    return wait(100).then(_ => a + 2);
}

function doSomething(a, b) {
    console.log('doSomething(' + a + ',' + b + ')');
}

Upvotes: 1

Related Questions