57

I am running a forEach loop on an array and making two calls which return promises, and I want to populate an object say this.options, and then do other stuff with it. Right now I am running into the async issue if i use the following code sample and i get into the then function first.

$.when.apply($, someArray.map(function(item) {
    return $.ajax({...}).then(function(data){...});
})).then(function() {
    // all ajax calls done now
});

This is working code below, but it only works for the first element in the array, because I call the resulting function in the .then of the response. I want to do all the fetch first for all elements of the array and then call the resulting function to do something.

array.forEach(function(element) {
    return developer.getResources(element)
        .then((data) = > {
            name = data.items[0];
            return developer.getResourceContent(element, file);
        })
        .then((response) = > {
            fileContent = atob(response.content);
            self.files.push({
                fileName: fileName,
                fileType: fileType,
                content: fileContent
            });
            self.resultingFunction(self.files)
        }).catch ((error) = > {
            console.log('Error: ', error);
        })
});

How do i populate the self.files object after the forEach loop is complete, and then call the resulting function with the files object?

TimoStaudinger
  • 41,396
  • 16
  • 88
  • 94
xyzzz
  • 1,463
  • 5
  • 18
  • 28
  • 3
    Why `forEach` when you clearly need `map`? – Yury Tarabanko Jul 13 '16 at 21:51
  • @YuryTarabanko I used a map and that didn't work either. I also tried the approach in the answer but with a `$.when.apply($, promises).then()`, but didn't work either. I think it did not work because I was did not do it the ES6 way. – xyzzz Jul 13 '16 at 23:11

5 Answers5

113

Promise.all() will be helpful here:

var promises = [];

array.forEach(function(element) {
    promises.push(
        developer.getResources(element)
            .then((data) = > {
                name = data.items[0];
                return developer.getResourceContent(element, file);
            })
            .then((response) = > {
                fileContent = atob(response.content);
                self.files.push({
                    fileName: fileName,
                    fileType: fileType,
                    content: fileContent
                });
            }).catch ((error) = > {
                console.log('Error: ', error);
            })
    );
});

Promise.all(promises).then(() => 
    self.resultingFunction(self.files)
);

This starts the AJAX call for each of the items, adds the result of each call to self.files once the call is complete and calls self.resultingFunction() after all calls have been completed.

Edit: Simplified based on Yury Tarabanko's suggestions.

Christopher Bottoms
  • 11,218
  • 8
  • 50
  • 99
TimoStaudinger
  • 41,396
  • 16
  • 88
  • 94
  • 2
    Should be `Promise.all`, not Promises (plural). – lucasjackson Jul 13 '16 at 21:53
  • 2
    Why are you manually creating a Promise? It all looks like `getResources` already returns one – Yury Tarabanko Jul 13 '16 at 21:57
  • @YuryTarabanko Out of curiosity, if you would push the promises returned from `getResources` directly to the `promises` array instead of wrapping it in your own, is it guaranteed that all individual finalization handlers are called first before the finalization handler of `Promise.all()`? If not, creating your own promises seems necessary. – TimoStaudinger Jul 13 '16 at 22:06
  • @TimoSta Not directly from `getResources` but the last promise you get in chain `getResources().then(fn1).then(fn2).catch()`. Every time you call `then` you get a new promise. And sure `fn1` will be called before `fn2` and before finalization handler. Simply because promises will pass the result of fn1 to fn2 and then to finalization handler (as corresponding array element). – Yury Tarabanko Jul 13 '16 at 22:16
  • Ah, I see. Learned something new today! Thanks :-) – TimoStaudinger Jul 13 '16 at 22:20
  • I agree with all of the above. I have worked with chaining promises before, but this woudn't work for some reason. It works now. Thanks for the solution guys. – xyzzz Jul 13 '16 at 23:13
  • 1
    I have a doubt. The list of all promises might not be in sequential order since array.forEach() is asynchronous. This might work for usual cases but what if one of the items in forEach delays response, then will the order of the promises in the promise array remain same ? – informer Sep 28 '17 at 20:30
  • @informer `forEach` is not asynchronous, and even if it were, I don't see how the order of the array matters? – TimoStaudinger Sep 28 '17 at 20:38
  • Right, I get it now. I confused multi-threading with async. The order of the forEach will be same as the order of elements. And 'promise' making sure that none of these statements are executed asynchronously. Cherry on top 'Promise.all' lets you execute your part of code once all these promises are resolved. Cool. (Y) – informer Sep 28 '17 at 20:47
  • @Timo , I would like to use every instead of forEach so that I could break out early. How could I do that with chain of promises? Thank you – Frank Mar 01 '18 at 00:59
  • @informer, What is the difference between multi-threading and async with forEach and Every? Thanks. – Frank Mar 01 '18 at 12:02
  • This was really helpful. The synchronous nature of forEach was throwing off my output. – O'Dane Brissett Jul 28 '20 at 17:51
47

Just a slight variation of the accepted solution above would be:

var promises = array.map(function(element) {
      return developer.getResources(element)
          .then((data) = > {
              name = data.items[0];
              return developer.getResourceContent(element, file);
          })
          .then((response) = > {
              fileContent = atob(response.content);
              self.files.push({
                  fileName: fileName,
                  fileType: fileType,
                  content: fileContent
              });
          }).catch ((error) = > {
              console.log('Error: ', error);
          })
});

Promise.all(promises).then(() => 
    self.resultingFunction(self.files)
);

I used Array.map instead of Array.forEach, which means I don't need to create an empty array first, I just re-use the existing one.

JackDev
  • 4,891
  • 1
  • 39
  • 48
8

You can use .map as a cycle to start Promise request for each element of array and return new Array of these Promises. Also I used destructurization, but not sure that we need it here.

await Promise.all([...arr.map(i => "our promise func"())]);
letUser
  • 181
  • 1
  • 3
  • While this code may answer the question, providing additional context regarding why and/or how this code answers the question improves its long-term value. – Abdul Aziz Barkat Feb 02 '21 at 11:12
4

The following code is simple understanding of sync using Promise.

let numArr = [1,2,3,4,5];
let nums=[];

let promiseList = new Promise(function(resolve,reject){
  setTimeout(()=>{
        numArr.forEach((val)=>{
        nums.push(val);
    });
    resolve(nums);
 },5000)
})


Promise.all([promiseList]).then((arrList)=>{
  arrList.forEach((array)=>{
    console.log("Array return from promiseList object ",array);    
  })

 });

Above example will hold array nums for 5 sec. and it will print on console after it release.

jasmeetsohal
  • 141
  • 1
  • 2
  • 11
2

You might look at this answer to a similar question for an excellent hint. The solution given there uses Array#reduce() to avoid having to accumulate all of the Promises before doing any of the work rather than using Promise.all().

Alan Mimms
  • 651
  • 9
  • 22