3

This is similar to, but not quite the same as How do I access previous promise results in a .then() chain?

I have a situation where I am making two async requests in parallel, followed by a third async request which depends on the success of the first two, and finally passing the results of the second async request to the function callback.

As of now I understand how to do this in two ways (.catch statements and function signatures omitted for brevity):

  1. Using scope closure (my current implementation)

    var foo;
    Promise.join(promiseA, promiseB, function(resultsA, resultsB) {
      foo = resultsB;
      return promiseC;
    })
    .then(function() {
      // foo is accessible here
      callback(null, foo);
    });
    
  2. Using Promise.bind, but have to use Promise.map instead of Promise.join

    var targetIndex = 1;
    Promise.resolve(promises)
      .bind({})
      .map(function(response, index) {
        if (index === targetIndex) {
          this.foo = response;
        }
      })
      .then(function() {
        return promiseC;
      })
      .then(function() {
        // this.foo is accessible here
        callback(null, this.foo);
      });
    

As you can tell, option 2 is rather ugly since I have to manually check if the index parameter of the mapper matches the index of the promise result that I care about. Option 1 uses scope closure, which I understand is undesirable in most cases (but seems to be my best option at this point).

What I would really like to do is something like:

Promise.bind({})
  .join(promiseA, promiseB, function(resultsA, resultsB) {
     this.foo = resultsB;
     return promiseC;
  })
  .then(function() {
    // I WISH this.foo WAS ACCESSIBLE HERE!
    callback(null, this.foo);
  });

Is there a way for me to utilize Promise.join instead of Promise.map to avoid using a scope closure in this situation?

Community
  • 1
  • 1
thedevkit
  • 33
  • 4

3 Answers3

1

You have a interesting use case since a Promise needs the result of a promise multiple steps back in the chain. For such a "backward" problem, I would recommend a "backward" solution; adding resultB back into the chain after promiseC:

Promise.join(promiseA, promiseB, function(resultA, resultB) {
  return promiseC.then(function() {
    return resultB;
  });
})
.then(function(resultB) {
  callback(null, resultB);
});

Ideally, promiseC should result in resultB, but that's now always possible.

Edit: Note that I didn't used nested promises on purpose here. The anonymous function is there only to pass values, not execute logic. This approach does the same thing:

...
return promiseC.then(function() {
  callback(null, resultB); // really not what you should be doing
});

but is discouraged because it added a layer of nested logic which ruins the design principle of chaining.

Edit 2: This can be achieved using bound closures like:

Promise.join(promiseA, promiseB).bind({})
.then(function(resultA, resultB) {
  this.resultB = resultB;
  return promiseC;
})
.then(function(resultC) {
  callback(null, this.resultB);
});
tcooc
  • 20,629
  • 3
  • 39
  • 57
  • I haven't tested this yet but, correct me if I'm wrong, also creates a scope closure since resultB is referenced in the anonymous function directly. I'd like to utilize "bound promises" as opposed to closures. The bluebird docs mention the disadvantages of scope closure here: [Promise.bind](https://github.com/petkaantonov/bluebird/blob/master/API.md#binddynamic-thisarg---promise) – thedevkit Sep 30 '15 at 20:32
  • @thedevkit Using `bind` is also a valid way of achieving this (updated answer with usage of bind). However, just to clear things up, the disadvantage of scope closure shown in the docs refers to method 1 in your question, where the shared variable is in the "outside" closure. The method I suggested creates a "inner" scope, which doesn't have those disadvantages. – tcooc Sep 30 '15 at 20:40
  • This is great! I noticed something wrong with the syntax, however. Instead of `.then(function(resultA, resultB)`, it should read: `.then(function(results)` , where results is an array and each index corresponds to the promise passed in at that index in the join statement. – thedevkit Oct 01 '15 at 00:35
  • `.then(function(resultA, resultB) {` should be `.spread(function(resultA, resultB) {`, otherwise you only get the first parameter and it is an array with two elements. – Le Roux Bodenstein Aug 09 '16 at 16:02
0

Node supports generators, let's utilize Bluebird's ability to their fullest with Promise.coroutine:

const yourFunciton = Promise.coroutine(function*(){
    // obtain other promises
    const a = yield getPromiseA(); // function that returns promiseA
    const b = yield getPromiseB(); // function that returns promiseB
    const c = yield calculatePromiseC(a, b); 
    return b; // or whatever value you want to return, or callback with
});
// call yourFunction, it returns a promise for the completion

The thing is, by using coroutines and modern NodeJS we are able to escape nesting and chaining completely, and can write asynchronous code in a straightforward synchronous way. We don't have to do any chaining or nested scoping at all since everything is at the same scope.

Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
0

This is similar to, but not quite the same as How do I access previous promise results in a .then() chain?

I think it's exactly the same. Just notice that your pattern with Promise.join

Promise.join(promiseA, promiseB, function(resultsA, resultsB) {
    return promiseC;
}).then(function(resultsC) {
    // how to get A or B here?
})

is equivalent to the "desugared" code

Promise.all([promiseA, promiseB])
.then(function([resultsA, resultsB]) { // ES6 destructuring syntax
    return promiseC;
}).then(function(resultsC) {
    // how to get A or B here?
})

Given that, we can apply all the solutions from there one-to-one.

  • Contextual state is horrible, and explicit pass-through is cumbersome, so I won't detail them here.
  • Nesting closures is easy and better than both your approaches:

    Promise.join(promiseA, promiseB, function(resultsA, resultsB) {
        return promiseC
        .then(function() {
            return resultsB;
        });
    }).then(callback.bind(null, null), callback);
    
  • Breaking the chain means that you'd just use Promise.join twice:

    var promiseC_ = Promise.join(promiseA, promiseB, function(resultsA, resultsB) {
        return promiseC
    });
    Promise.join(promiseC_, promiseB).then(function(_, resultsB) {
        return resultsB;
    }).then(callback.bind(null, null), callback);
    
  • async/await is the future, and if you use a transpiler anyway you should go for it:

    (async function() {
        var [resultsA, resultsB] = await Promise.all([promiseA, promiseB]);
        var resultsC = await promiseC;
        return resultsB;
    }()).then(callback.bind(null, null), callback);
    
  • but if you don't want a transpiler yet work in ES6, you can already use Bluebird with generators:

    Promise.coroutine(function* () {
        var [resultsA, resultsB] = yield Promise.all([promiseA, promiseB]);
        var resultsC = yield promiseC;
        return resultsB;
    })().then(callback.bind(null, null), callback);
    
Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Wow, this is a much more thorough response than I expected. Forgive me if I sound like a noob, but I noticed that your nested closure solution is almost identical to tcooc's first solution, however you utilized `.then(callback.bind(null,null),` when fulfilling the outer promise. Is there any improvement with this approach over `.then(function(resultB)`, or is it style preference? – thedevkit Oct 01 '15 at 21:45
  • I just chained everything in the end, including an error callback - that `bind` thingy is equivalent to `.then(function(res) { callback(null, res); }, function(err) { callback(err); })`. Directly calling `callback(resultB)` instead of `return`ing and the chaining doesn't make much a difference, but the error handler does (and it cannot go inside). Therefore I prefer the "*always returning*" style :-) – Bergi Oct 01 '15 at 21:53