3

I'm kind of a JS noob, but so far I really like the ES6 / React / Immutable capability to do functional programming and FRP, and in particular, the promise api. I particularly like the pattern of chaining .then's, eg, somePromiseGenerator().then(...).then(...).catch(...). This is perfect when the flow of asynchronous calls is perfectly linear. However, Often I want to pass results from the top of this chain to the end.

The sub-optimal pattern I've been using looks like:

somePromiseGenrator()
    .then(r => {
        const inter = makeSomeIntermediateResults(r);
        const newPromise = makeNewPromise(r);

        return Promise.all([newPromise, Promise.resolve(inter)]);
    })
    .then(r => {
        handleR0andR1(r[0], r[1]);
    })

this happens, for instance, when i get data from ElasticSearch and want to supplement it with stuff that's in SQL or Neo4J or call out to a secondary API.

Using Promise.all and especially Promise.resolve seems like a waste of effort. Is there a better way to do this work?

downer
  • 954
  • 2
  • 13
  • 24
  • `.then{r => { ... }}` should be `.then(r => { ... })` for both chains. – Patrick Roberts Jul 28 '17 at 20:38
  • Also, could you give an example of some "ideal but non-existent or unknown syntax" that you'd like to see? I'm not really seeing the problem, other than the non-linearity making you use an array in your resolved value, which in itself isn't really an issue in my opinion. – Patrick Roberts Jul 28 '17 at 20:42
  • you could do newPromise.then(...) instead of returning and chaining, but the way you have it is fine honestly – aw04 Jul 28 '17 at 20:44
  • Since you mention "pass results from the top of this chain to the end", you may be interested in this [How to chain and share prior results](https://stackoverflow.com/questions/28714298/how-to-chain-and-share-prior-results-with-promises/28714863#28714863). – jfriend00 Jul 28 '17 at 21:58

3 Answers3

4

Using array destructuring to name variables:

somePromiseGenrator()
    .then(r => {
        const inter = makeSomeIntermediateResults(r);
        const newPromise = makeNewPromise(r);

        return Promise.all([newPromise, inter]);
    })
    .then([newPromiseResult, intermediateResult]) => {
        handleR0andR1(newPromiseResult, intermediateResult);
    })

That alone is much clearer to follow, and I also removed the redundant Promise.resolve(), as non-promise values in the array passed to Promise.all() will automatically be wrapped in that already.

Using async / await:

Assuming your version of Node.js is >= 7.6 (or you're using a transpiler / bundler / polyfill that supports ES2016 features), you can convert the following:

function promiseChain() {
    return somePromiseGenrator()
        .then(r => {
            const inter = makeSomeIntermediateResults(r);
            const newPromise = makeNewPromise(r);

            return Promise.all([newPromise, inter]);
        })
        .then([newPromiseResult, intermediateResult]) => {
            return handleR0andR1(newPromiseResult, intermediateResult);
        })
}

into this:

async function promiseChain() {
    const r = await somePromiseGenerator();
    // I'm assuming this isn't a promise
    // since it was initially wrapped in Promise.resolve()
    const inter = makeSomeIntermediateResults(r);
    const newPromiseResult = await makeNewPromise(r);

    return handleR0andR1(newPromiseResult, inter);
}
Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
  • Using ES7's `async` and `await` is definitely the way to go, but using `Promise.all` in ES6 is not a good idea when you have to pass data down two or more promises in a promise chain. – Derek 朕會功夫 Jul 28 '17 at 21:21
  • @Derek朕會功夫 My first point was just to present some features like destructuring and implicit `Promise.resolve()` wrapping that would make the code more readable without actually changing how it worked. I was easing OP into a more revolutionary change like `async` / `await` rather than just throwing that out right off the bat. – Patrick Roberts Jul 28 '17 at 21:25
  • Definitely a great answer (upvoted), but I think the question specifically tagged ES6 (though I believe Babel does have support for ES7 syntax.) – Derek 朕會功夫 Jul 28 '17 at 21:28
  • something like this is what I was looking for. much cleaner! – downer Jul 29 '17 at 00:33
  • 1
    ES6 as a tag may just be due to the fact that I'm not yet familiar with es7. like I said, I've only just started programming JS – downer Jul 29 '17 at 00:34
0

I guess you can do something like this if you don't prefer using Promise.all:

function pack(){
    var args = arguments;
    if((args.length - 1) % 2 != 0){
        throw "Invalid input";
    }
    var obj = args[0],
        promises = [];
    for(var i = 1; i < args.length; i+=2){
        let promise = args[i],
            name = args[i+1];
        promises.push(promise.then(val => {
            obj[name] = val;
        }));
    }
    return Promise.all(promises).then(() => obj);
}

Then you can use it this way:

Promise
.resolve({})
.then(obj => {
    return pack(obj, genVal("a"), "a", genVal("b"), "b");
})
.then(obj => {
    return pack(obj, genVal("c"), "c");
})
.then(console.log);

/* {
       partA: "some value from part A",
       partB: "some value from part B",
       partC: "some value from part C"
   } */

https://jsfiddle.net/DerekL/ssr23mjy/

Derek 朕會功夫
  • 92,235
  • 44
  • 185
  • 247
  • Can you explain `console.log` ? I don't really have any knowledge on ES6, and randomly just saw this question, went to your fiddle, and oddly enough saw the just doing `console.log` was working. How is console logging something when you didn't pass anything? – jdmdevdotnet Jul 28 '17 at 20:51
  • @Derek maybe you could use this example to try and stay relevant to the given scenario in the question? https://jsfiddle.net/ssr23mjy/1/ – Patrick Roberts Jul 28 '17 at 20:53
  • `Promise.then` will execute the function reference in the argument as well as passing the return value from the previous promise (ie `obj`). Think of it as `var callback = console.log; callback.call(this, rtn)`. – Derek 朕會功夫 Jul 28 '17 at 20:53
  • @PatrickRoberts If you look closely at the given code `return Promise.all([newPromise, Promise.resolve(inter)])`, it is the exact same as `return Promise.all([newPromise, inter])` which you can use two `.then` and `pack` to achieve. – Derek 朕會功夫 Jul 28 '17 at 20:56
  • @Derek朕會功夫 that is wrong. Using two `.then()` will resolve the asynchronous requests _sequentially_ while `Promise.all()` will resolve the asynchronous requests _in parallel_. Chaining `.then()` is therefore very different and typically much slower in practice due to the lack of parallelization. – Patrick Roberts Jul 28 '17 at 20:57
  • 1
    @PatrickRoberts You are completely correct that they technically aren't the same, but my answer is really to his statement of *"often I want to pass results from the top of this chain to the end"*. – Derek 朕會功夫 Jul 28 '17 at 21:00
0

You could define inter as a variable that has an extended scope, e.g. make it a parameter in an immediately invoked function expression:

(inter => somePromiseGenerator()
    .then(r => {
        inter = makeSomeIntermediateResults(r);
        return makeNewPromise(r);
    })
    .then(r => handleR0andR1(r, inter))
)();

See also the options offered in this accepted answer, of which mine is a variant on option 2.

trincot
  • 317,000
  • 35
  • 244
  • 286