How to use promise in forEach loop of array to populate an object
Asked Answered
N

6

63

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?

Nomen answered 13/7, 2016 at 21:45 Comment(2)
Why forEach when you clearly need map?Splint
@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.Nomen
D
119

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.

Decibel answered 13/7, 2016 at 21:50 Comment(13)
Should be Promise.all, not Promises (plural).Alber
Why are you manually creating a Promise? It all looks like getResources already returns oneSplint
@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.Decibel
@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).Splint
Ah, I see. Learned something new today! Thanks :-)Decibel
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.Nomen
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 ?Binny
@Binny forEach is not asynchronous, and even if it were, I don't see how the order of the array matters?Decibel
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)Binny
@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 youHypothermal
@informer, What is the difference between multi-threading and async with forEach and Every? Thanks.Hypothermal
This was really helpful. The synchronous nature of forEach was throwing off my output.Pancake
That works with jQuery or Axios and other. Just use promises.push(return jQuery.ajax({ your code }())Nicolella
T
49

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.

Timbre answered 10/8, 2017 at 10:50 Comment(2)
Could you highlight what you have changed so that we don't have to read every line?Trina
Have added a new sentence at the end explaining what I did.Timbre
F
10

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"())]);
Fault answered 2/2, 2021 at 11:0 Comment(1)
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.Boniface
E
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.

Ewan answered 22/3, 2018 at 11:57 Comment(0)
W
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().

Winegar answered 17/8, 2017 at 21:43 Comment(0)
L
0

Here's a version using async/await. If the chain of promises is longer it might be neater.

const promises = array.map(async(element) {
         try{
             const data = await developer.getResources(element)
             const 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);
        }
    });
    
    await Promise.all(promises) 
    self.resultingFunction(self.files)
Lacefield answered 22/11, 2023 at 14:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.