0

In the following code, expiringContentsAllBU is populated within async/await function getOnlyContentWhatsNewReport which is inside the array.map(), but expiringContentsAllBU becomes empty when accessing it outside the businessUnits.map() function.

const getExpiringContents = async () => {
  let businessUnits = Object.keys(contentFulSpaces);  
  let expiringContentsAllBU = [];
  businessUnits.map( async (bu) => {
      await getOnlyContentWhatsNewReport(bu, (err, result) => { 
        if(result) {
          let expiringContentsByBU = {};
          expiringContentsByBU['businessUnit'] = bu;
          expiringContentsByBU['expiringContents'] = result;
          expiringContentsAllBU.push(JSON.parse(JSON.stringify(expiringContentsByBU)));
        } else console.log('No expiring contents found for WhatsNewSection');
      })
    });
    console.log('result', expiringContentsAllBU);
}
Prem
  • 5,685
  • 15
  • 52
  • 95
  • 2
    map is synchronous method, you can't use async code in callbacks – deathangel908 Apr 24 '19 at 16:19
  • Why are you using using `map()` for this? Why not just normal iteration? – jmkmay Apr 24 '19 at 16:20
  • @deathangel908 You can, but the result will be an array of promises. If you'd use `Promise.all`, that's usually even desirable. – Bergi Apr 24 '19 at 16:20
  • `getOnlyContentWhatsNewReport` does not appear to return a promise, since it takes a callback. That means you cannot `await` anything. – Bergi Apr 24 '19 at 16:21
  • @Bergi, I didn't say that js won't compile. in Example above map makes no sense, it's better to use forEach – deathangel908 Apr 24 '19 at 16:25
  • map (same forEach) will not be sequential if this is what you want. For sequential await you need to use `for await of` or normal `for` with `await` inside. If you don't care about the order of requests in map you need to use `Promise.all(businessUnits.map(...` – jcubic Apr 24 '19 at 16:25
  • @deathangel908 And I was saying that [`forEach` makes less sense than `map`](https://stackoverflow.com/a/37576787/1048572) – Bergi Apr 24 '19 at 17:44
  • @Bergi the author didn't await the loop, nor returned anything from it. – deathangel908 Apr 24 '19 at 21:59
  • @deathangel908 The `async` callback function returns a promise (for `undefined`, but still) – Bergi Apr 25 '19 at 09:24

4 Answers4

2

var getOnlyContentWhatsNewReport = Promise.resolve(123);

const getExpiringContents = async () => {
  let businessUnits = [{ id: 1 }, { id: 2 }, { id: 3 }];  
  const expiringContentsAllBU = await Promise.all(businessUnits.map(async (bu) => {
      return getOnlyContentWhatsNewReport.then(respBu => {
        bu.businessUnit = respBu;
        return bu;
      }).catch((err) => {
        console.log('No expiring contents found for WhatsNewSection');
        return null;
      });
   }));
   console.log('result', expiringContentsAllBU);
}

getExpiringContents();

You have to wait until the map completes and all callbacks are done. The console.log and the subsequent code blocks will be executed before your map completes, so

const getExpiringContents = async () => {
  let businessUnits = Object.keys(contentFulSpaces);  
  const expiringContentsAllBU = await Promise.all(businessUnits.map(async (bu) => {
      return getOnlyContentWhatsNewReport(bu, (err, result) => { 
        if(result) {
          let expiringContentsByBU = {};
          expiringContentsByBU['businessUnit'] = bu;
          expiringContentsByBU['expiringContents'] = result;
          return JSON.parse(JSON.stringify(expiringContentsByBU);
        } else {
          console.log('No expiring contents found for WhatsNewSection');
          return null;
        }
      })
   }));
   console.log('result', expiringContentsAllBU);
}
Avanthika
  • 3,984
  • 1
  • 12
  • 19
  • But it prints `undefined` – Prem Apr 24 '19 at 16:44
  • @Prem, I don't know what you do inside `getOnlyContentWhatsNewReport`, but I have edited my answer and have posted a snippet to show how we can do it. The `return` is for inner function, you have to return the inner function response to main function to see the result. – Avanthika Apr 24 '19 at 17:01
1

As map is not aware of async functions you need to use something that is. One example is the Bluebird Promise.map equivalent:

const getExpiringContents = async () => {
  let businessUnits = Object.keys(contentFulSpaces);  

  // Use Promise.map here to convert each businessUnit entry into a record
  let expiringContentsAllBU = await Promise.map(businessUnits, async (bu) => {
    await getOnlyContentWhatsNewReport(bu, (err, result) => { 
      if (!result) {
        console.log('No expiring contents found for WhatsNewSection');
        return;
      }

      let expiringContentsByBU = {};
      expiringContentsByBU['businessUnit'] = bu;
      expiringContentsByBU['expiringContents'] = result;

      return JSON.parse(JSON.stringify(expiringContentsByBU));
    })
  });

  // As this may contain undefined elements, remove those
  expiringContentsAllBU = expiringContentsAllBU.filter(e => e);

  console.log('result', expiringContentsAllBU);
}

You could flatten this code a bit more if you made getOnlyContentWhatsNewReport return a promise as it should instead of using a callback method. async won't wait on callback methods so be sure that also returns a promise or this code won't wait properly.

A fully promisized version that's refactored a litlte more looks like this:

const getExpiringContents = async () => {
  let businessUnits = Object.keys(contentFulSpaces);  

  let expiringContentsAllBU = await Promise.map(businessUnits, async businessUnit => {
    let expiringContents = await getOnlyContentWhatsNewReport(businessUnit);

    if (!expiringContents) {
      console.log('No expiring contents found for WhatsNewSection');
      return;
    }

    // Choosing names that match the output means you can use the destructuring operator
    let expiringContentsByBU = {
      businessUnit,
      expiringContents
    };

    // A more formalized "clone" function could help here.
    return JSON.parse(JSON.stringify(expiringContentsByBU));
  });

  // As this may contain undefined elements, remove those
  expiringContentsAllBU = expiringContentsAllBU.filter(e => e);

  console.log('result', expiringContentsAllBU);
}
tadman
  • 208,517
  • 23
  • 234
  • 262
0

You can either change your .map() to a for loop.

Or in your .map() function, return promises. Then you can call await Promise.all(promiseArray)

d-_-b
  • 21,536
  • 40
  • 150
  • 256
0

Using async with some lodash function for sanity will help -

getExpiringContents = async() => {
  let businessUnits = Object.keys(contentFulSpaces);

  let expiringContentsAllBU = await Promise.map(businessUnits, async businessUnit => {
    let expiringContents = await getOnlyContentWhatsNewReport(businessUnit);

    if (_.isEmpty(expiringContents)) {
      console.log('No expiring contents found for WhatsNewSection');
      return;
    }

    // Choosing names that match the output means you can use the destructuring operator
    let expiringContentsByBU = {
      businessUnit,
      expiringContents
    };

    // A more formalized "clone" function could help here.
    return _.cloneDeep(expiringContentsByBU);
  });

  // As this may contain undefined elements, remove those
  expiringContentsAllBU = _.compact(expiringContentsAllBU);

  console.log('result', expiringContentsAllBU);
}