简体   繁体   中英

Updating async forEach to update every document property based off property from another collection

I have this piece of code that works. However, I am not sure why and I feel like it might behave inconsistently.

  await Listing.find({}, (err, listings) => {
    if (err) {
      console.log(err);
    }
    listings.forEach(async (listing) => {
      //console.log(listing);
      let championsUpdate = {};
      for (let key in listing["champions"]) {
        championsUpdate[key] = rankingDB[key];
      }

      await Listing.updateOne(
        { _id: listing._id },
        { $set: { champions: championsUpdate } }
      );
    });
  });

Pretty much I am finding all the listings that I need to update, and then for each listing I am updating one of the properties based off the data I retrieved earlier.

So far it's been behaving appropriately but I remember being told to avoid using async await in a forEach loop because it does not behave as we expect. But I can't figure out why this is working and if I should avoid the forEach and use a forOf. I am also worried about having nested async awaits.

Does anyone know if something like this is ok? For more context on my application

Because the callback in the forEach loop is async, things that follow the call to forEach may execute before forEach finishes, and it will not wait for each iteration to finish before continuing.
For example the await in front of the updateOne call is actually pointless, since the outer async function isn't awaited on, which shows that it is probably not behaving the way you intend it to.

The reason it is recommended that you not use async inside a forEach is that it is almost never behaving the way you intend it to, or you don't actually need it, or you may forget that you called it that way later and unintentionally cause a race condition later (ie: it makes the execution order hard to reason about and virtually unpredictable). It has valid use if you actually do not care about the results of the call, when they are called, and when they resolve.

Below is a demo showing how it can cause unexpected results. sleep with random time to make each .updateOne call resolve at random times. Notice that call to console.log(`Will execute before...`) executes before the forEach iterations.

 async function runit() { await Listing.find({}, (err, listings) => { if (err) { console.log(err); } listings.forEach(async (listing) => { //console.log(listing); let championsUpdate = {}; for (let key in listing["champions"]) { championsUpdate[key] = rankingDB[key]; } await Listing.updateOne( { _id: listing._id }, { $set: { champions: championsUpdate } } ); }); }); console.log(`Will execute before the updateOne calls in forEach resolve`) } runit()
 <script> // mock data with sequential _id from 0...9 listings = Array(10).fill().map((_,_id)=>({_id, champions: {1:1,2:2}})) rankingDB = Array(10).fill({1:1.1,2:2.2}) // Promise that resolves in ms milliseconds function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // mock Listing Listing = { find(x, fn) { console.log('find') return new Promise(res=> res(fn(undefined,listings))) }, // updateOne resolves after random time < 1000 ms async updateOne({_id}) { await sleep(Math.random()*1000) console.log('updateOne with listing _id=',_id) } } </script>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM