3

I am writing a Firebase Cloud Function in node.JS that reads user data from Firebase Firestore. I am unable to push the token values into the token array and return all tokens at the end of the function. My code is as follows:

function getTokens(subscribers) {
    return new Promise(async function (resolve, reject) {
        const tokenArray = [];
        subscribers.forEach(async (subscriber) => {
            await firestore
                .collection('users')
                .doc(subscriber)
                .get()
                .then((user) => {
                    console.log("Getting user data for user: ", user);
                    const tokens = user.data().tokens;
                    if (tokens) {
                        tokens.forEach((token) => {
                            console.log("Adding token to token array"); // data is available here but does not push into tokenArray
                            tokenArray.push(token);
                        });
                    }
                }).catch((error) => { console.error(error); reject(error); });
        });
        console.log("Token Array -- Final: ", tokenArray);
        resolve(tokenArray);
    });
};
Javax.tech
  • 45
  • 9

2 Answers2

3

The OP code can be corrected and made more concise as follows:

async function getTokens(subscribers) {
  const getUser = subscriber => firestore.collection('users').doc(subscriber).get();
  const promises = subscribers.map(getUser);
  const users = await Promise.all(promises);
  return users.map(user => user.data().tokens).flat()
}

Some notes:

  • decorate the function as async, since it includes an await
  • don't create any extra promise with Promise.new(), Firestore's get() returns a promise
  • collect get promises in an array with map, and run them with Promise.all()
  • mapping the resulting users' data.tokens produces an array of arrays. Flatten it and you're done.
  • a catch that only throws, is just like having no catch
danh
  • 62,181
  • 10
  • 95
  • 136
2

You cannot use async-await in a forEach loop. Try mapping an array of promises and then using Promise.all() as shown below:

function getTokens(subscribers) {
  return new Promise(async function (resolve, reject) {
    
    const subscribersDocs = await Promise.all(
      subscribers.map((subscriber) => {
        return firestore.collection("users").doc(subscriber).get();
      })
    );


    const tokenArray = subscribersDocs.reduce((acc, curr) => {
      const tokens = curr.data().tokens;
      if (tokens) {
        acc = acc.concat(tokens);
      }
      return acc;
    }, []);

    console.log("Token Array -- Final: ", tokenArray);
    resolve(tokenArray);
  });
}

Also checkout: Using async/await with a forEach loop

Dharmaraj
  • 47,845
  • 8
  • 52
  • 84
  • 2
    A couple performance points: if your ```subscribers``` at the start is not an totally arbitrary list, it would be better to use a query to select them. Same cost in `reads`, but only one round trip. Similarly, rather than looping over ```tokens``` and using push(), you should be able to use ```.concat()``` – LeadDreamer Dec 03 '21 at 17:57
  • 1
    Thanks @LeadDreamer. I should have mentioned the `in` operator though it's useful only if the array has upto 10 elements. – Dharmaraj Dec 03 '21 at 17:59