18
let currentProduct;

for (let i = 0; i < products.length; i++) { 
    currentProduct = products[i];

    subscription.getAll(products[i]._id)
        .then((subs) => {
            update(subs, currentProduct);
        });
}

I'm using bluebird, the methods getAll and update return promises. How can I say "Wait until the two promises return, then update the currentProduct value"? I'm quite new to JS...

Jumpa
  • 4,319
  • 11
  • 52
  • 100

3 Answers3

31

This will be straightforward if you can use async/await:

// Make sure that this code is inside a function declared using
// the `async` keyword.
let currentProduct;

for (let i = 0; i < products.length; i++) { 
    currentProduct = products[i];

    // By using await, the code will halt here until
    // the promise resolves, then it will go to the
    // next iteration...
    await subscription.getAll(products[i]._id)
        .then((subs) => {
            // Make sure to return your promise here...
            return update(subs, currentProduct);
        });

    // You could also avoid the .then by using two awaits:
    /*
    const subs = await subscription.getAll(products[i]._id);
    await update(subs, currentProduct);
    */
}

Or if you can only use plain promises, you can loop through all your products, and put each promise in the .then of the last loop. In that way, it will only advance to the next when the previous has resolved (even though it will have iterated the whole loop first):

let currentProduct;

let promiseChain = Promise.resolve();
for (let i = 0; i < products.length; i++) { 
    currentProduct = products[i];

    // Note that there is a scoping issue here, since
    // none of the .then code runs till the loop completes,
    // you need to pass the current value of `currentProduct`
    // into the chain manually, to avoid having its value
    // changed before the .then code accesses it.

    const makeNextPromise = (currentProduct) => () => {
        // Make sure to return your promise here.
        return subscription.getAll(products[i]._id)
            .then((subs) => {
                // Make sure to return your promise here.
                return update(subs, currentProduct);
            });
    }

    // Note that we pass the value of `currentProduct` into the
    // function to avoid it changing as the loop iterates.
    promiseChain = promiseChain.then(makeNextPromise(currentProduct))
}

In the second snippet, the loop just sets up the entire chain, but doesn't execute the code inside the .then immediately. Your getAll functions won't run until each prior one has resolved in turn (which is what you want).

CRice
  • 29,968
  • 4
  • 57
  • 70
  • 1
    If you use `await`, you would also use it instead of the `then` call – Bergi Dec 28 '17 at 21:18
  • 1
    @Bergi You're right. You could use await to get the result of `getAll`, then pass it to `update` on the next line with another await. But what's there is still valid and I've been known to mix and match my awaits and thens. I would say it's up to OP's discretion as to which style he prefers. – CRice Dec 28 '17 at 21:26
  • @Jumpa I've edited the post to include an example of what a pair of awaits would look like, see the commented section in the first snippet. – CRice Dec 28 '17 at 21:35
  • The promise chain would be simplest with recursion and a little less simple using reduce (see my answer). That answer also resolves or rejects to something something sane, especially when rejected because you need to know how far it got. – HMR Dec 29 '17 at 05:37
  • @CRice Using `await` is definitely cleaner. You should not use `then` inside an `async` function unless you need the second argument. – Bergi Dec 29 '17 at 08:28
  • How to catch the moment while this promises cycle finished? I mean I need to add some code after all promises processing finished(after last "then" finished). How can I do that? – kkost Mar 19 '18 at 21:57
  • 1
    @neustart47 Since each "then" chains off the last one, just add it in the `.then` of the promise chain after the loop has finished. eg, *after the loop*: `promiseChain.then(() => {/* do your thing */})` – CRice Mar 19 '18 at 22:00
11

Here is how I'd do it:

for (let product of products) { 
  let subs = await subscription.getAll(product._id);
  await update(subs, product);
}

No need to manually chain promises or iterate arrays by index :)

Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • 2
    I was trying your code and I think it's the more elegant one. Anyway you're missing "let" for product inside the for. I noticed that because I was getting a UnhandledPromiseRejectionWarning...could you please edit your code and add something to handle a promise rejection? Many thanks in advance. EDIT: nevermind I should use try/catch... – Jumpa Dec 29 '17 at 09:34
1

You may want to keep track of what products you've processed because when one fails you have no idea how many succeeded and you don't know what to correct (if roll back) or retry.

The async "loop" could be a recursive function:

const updateProducts = /* add async */async (products,processed=[]) => {
  try{
    if(products.length===0){
      return processed;
    }
    const subs = await subscription.getAll(products[0]._id)
    await update(subs, product);
    processed.push(product[0]._id);  
  }catch(err){
    throw [err,processed];
  }
  return await updateProducts(products.slice(1),processed);
}

Without async you can use recursion or reduce:

//using reduce
const updateProducts = (products) => {
  //keep track of processed id's
  const processed = [];
  return products.reduce(
    (acc,product)=>
      acc
      .then(_=>subscription.getAll(product._id))
      .then(subs=>update(subs, product))
      //add product id to processed product ids
      .then(_=>processed.push(product._id)),
    Promise.resolve()
  )
  //resolve with processed product id's
  .then(_=>processed)
  //when rejecting include the processed items
  .catch(err=>Promise.reject([err,processed]));
}

//using recursion
const updateProducts = (products,processed=[]) =>
  (products.length!==0)
    ? subscription.getAll(products[0]._id)
      .then(subs=>update(subs, product))
      //add product id to processed
      .then(_=>processed.push(products[0]._id))
      //reject with error and id's of processed products
      .catch(err=>Promise.reject([err,processed]))
      .then(_=>updateProducts(products.slice(1),processed))
    : processed//resolve with array of processed product ids

Here is how you'd call updateProducts:

updateProducts(products)
.then(processed=>console.log("Following products are updated.",processed))
.catch(([err,processed])=>
  console.error(
    "something went wrong:",err,
    "following were processed until something went wrong:",
    processed
  )
)
HMR
  • 37,593
  • 24
  • 91
  • 160