Home > Software engineering >  'Unexpected token' when recursively calling async function in Nodejs
'Unexpected token' when recursively calling async function in Nodejs

Time:03-05

My app contains posts with nested comments in Firebase Firestore structured such that each post/comment with docID has a sub collection postComments. Thus, a given post/comment can have an infinite number of nested comments.

comments
 - docID
   postComments
    - docID
    - docID
    - docID
 - docID
   postComments
    - docID
    - docID

I am currently writing a Firebase cloud function to recursively query all documents and sub collection documents of a given docID and return all of those documents in an array. My plan was to define the getChildComments async function which takes in a docID and returns all of the documents in that document's postComments sub collection. I would then recursively call getChildComments until I have built an array with all of the nested comments in a thread.

exports.loadWholeCommentThread = functions.https.onCall(async (data, context) => {
       let comments = await getChildComments(data.rootID);
       return comments;
});

async function getChildComments(docID) {
  try {
     const db = admin.firestore();
     const commentsRef = db.collection('comments').doc(docID).collection('postComments');
     var comments = [];
     const commentsQuerySnap = await commentsRef.get();

     commentsQuerySnap.forEach((comment) => {
       let commentData = comment.data();
       comments.push(commentData);
       if (commentData.commentCount > 0) {
         let childComments = await getChildComments(commentData.commentID);
         comments.concat(childComments);
       }
     });

     return comments;
  } catch (error) {
     functions.logger.log(error);
     throw new functions.https.HttpsError('unknown', 'ERROR0', { message: error.message } )
  }
}

Unfortunately, when I try to deploy my code, I get the error Parsing error. Unexpected token getChildComments on the line where I recursively call getChildComments inside of getChildComments. Removing the await from this line fixes the build issue but then the recursive call doesn't finish.

How should I fix my issue? Or is there a better way to query all nested documents?

CodePudding user response:

This is because you have used await outside of an async function (note that it is inside an arrow function!).

const comments = [];
const commentsQuerySnap = await commentsRef.get();

commentsQuerySnap.forEach((comment) => {
  let commentData = comment.data();
  comments.push(commentData);
  if (commentData.commentCount > 0) {
    let childComments = await getChildComments(commentData.commentID); // the keyword "await" here is invalid
    comments = comments.concat(childComments);
  }
});

But you can't just add async to this arrow function, because then your code won't properly wait for the comments array to be filled.

To properly fix this, you need to use .map() on the commentsQuerySnap.docs array in addition to using Promise.all to wait for each comment (and its children) to be retrieved.

const comments = [];
const commentsQuerySnap = await commentsRef.get();

await Promise.all(
  commentsQuerySnap.docs.map(
    async (comment) => {
      let commentData = comment.data();
      comments.push(commentData);
      if (commentData.commentCount > 0) {
        let childComments = await getChildComments(commentData.commentID);
        comments = comments.concat(childComments);
      }
    })
  )
);

While that above block works, the comments array may be out of order to what you were expecting. If you must maintain order of the comments fetched so they are in the same order as the query, you should return the built comments array for each document and then flatten them after they all have been retrieved.

// removed const comments = []; here
const commentsQuerySnap = await commentsRef.get();

const arrayOfCommentThreads = await Promise.all(
  commentsQuerySnap.docs.map(
    async (comment) => {
      let commentData = comment.data();
      const commentThread = [commentData];
      if (commentData.commentCount > 0) {
        let childComments = await getChildComments(commentData.commentID);
        commentThread = commentThread.concat(childComments);
      }
      return commentThread;
    })
  )
);

const comments = arrayOfCommentThreads.flat();

Personally, I prefer the spread operator to using .concat like so:

const commentsQuerySnap = await commentsRef.get();

const arrayOfCommentThreads = await Promise.all(
  commentsQuerySnap.docs.map(
    async (comment) => {
      const commentData = comment.data();
      if (commentData.commentCount === 0) {
        return [commentData];
      }
      
      const childComments = await getChildComments(commentData.commentID);
      return [commentData, ...childComments];
    })
  )
);

const comments = arrayOfCommentThreads.flat();
  • Related