Reputation: 3978
I have this function, that iterates through an array of strings, where each string represents a user's UID. The function is supposed to go to each user's profile, check for their most current reputation (same like SO), and then push a map of uid and reputation to a new array.
The second array always came out empty so I've placed a few logs to check what is happening. This is my function:
candidates.forEach(async id => {
console.log('user log ' + id);
const snap2 = await db.doc('communities_data/' + community.id + '/profiles/' + id).get();
const user = snap2.data();
console.log('user ' + user);
if (user !== undefined) {
console.log(user + ' just user');
const reputation = (user.reputation as number);
candidatesWithReputation.push(new UserAndReputation(id, reputation));
} else {
console.log('user undefined ');
};
});
the first which says 'user log ' +id
always prints and it prints the id of the user as it should, so I know the first array is just fine.
But none of the other logs print. ever. My first thought was that maybe I got the path wrong? But I've checked a million times, this is the path in which a user's profile would be in my DB.
for example, this could be a path to a profile:
communities_data/hfd98HDKKhfEwe6W/profiles/bqSFS04LKJDbfhdwU
Any idea where it fails
Upvotes: 0
Views: 133
Reputation: 708056
My guess is that you have a timing issue. .forEach()
does not wait for your asynchronous operations to complete before going on to the next iteration (it does not look at the promise that your code is returning from the async callback) and thus after the .forEach()
your second array will always be empty after the .forEach()
loop because it has not yet been populated (the asynchonous calls in the loop have not yet finished).
So basically, you rarely ever want to use async/await
inside a .forEach()
callback because the loop doesn't respect it and you have no way of knowing outside the loop when everything is done.
While you don't show a larger context for this code, the usual solution here is to use a regular for
lop or a for/of
loop which will wait for the await
statements and thus you can more easily know when everything is done.
Here's one way to do that:
async function someFunction() {
// other code here
for (let id of candidates) {
try {
console.log('user log ' + id);
const snap2 = await db.doc('communities_data/' + community.id + '/profiles/' + id).get();
const user = snap2.data();
console.log('user ' + user);
if (user !== undefined) {
console.log(user + ' just user');
const reputation = (user.reputation as number);
candidatesWithReputation.push(new UserAndReputation(id, reputation));
} else {
console.log('user undefined ');
};
} catch(e) {
console.log(e);
// decide what to do upon error,
// skip it and proceed?
// stop further processing?
}
}
// candidatesWithReputation should now be valid here
console.log(candidatesWithReputation);
// other code here
}
Note that the containing function has to be declared async
to allow you to use await
inside the for
loop.
For possibly better performance, you could also do all these lookups in parallel and use Promise.all()
to see when they're all done:
function someFunction() {
// other code here
Promise.all(candidates.map(id => {
return db.doc('communities_data/' + community.id + '/profiles/' + id).get().then(snap2 => {
return snap2.data();
}).catch(err => {
// decide what to do about an error here
// this implementation skips any queries with error and proceeds with the others
return undefined;
});
})).then(users => {
let candidatesWithReputation = [];
for (user of users) {
if (user !== undefined) {
// I've not seen this "user.reputation as number" syntax?? Typescript?
const reputation = (user.reputation as number);
candidatesWithReputation.push(new UserAndReputation(id, reputation));
}
}
return candidatesWithReputation;
}).then(users => {
// list of users with reputation here
console.log(users);
// further processing here
}).catch(err => {
console.log(err);
});
}
Upvotes: 5