Pushing to array in asynchonous map - javascript

I know this question has already been asked but i just can't manage to make this right. Tried using promises but to no avail. When console logging req.user items is still an empty array. I know I should use promises but I am having trouble implementing it. Help appreciated
app.get('/cart',checkAuthenticated, async (req, res) => {
if(req.user.cart.length > 0){
req.user.items = []
req.user.cart.map(async (item) => {
var itemDescription = await productsModel.findOne({id: item.itemId})
req.user.items.push(itemDescription)
});
console.log(req.user)
}

The reason it's empty it's because you don't await for all the async functions in the map to finish. Try this:
await Promise.all(req.user.cart.map(async (item) => {
var itemDescription = await productsModel.findOne({id: item.itemId})
req.user.items.push(itemDescription)
}));
Note as #jfriend00 has commented this implementation will not guarantee the order of items in req.user.items.
Because you are already using map it's just simpler to do the following and it also guarantees the order of items:
req.user.items = await Promise.all(req.user.cart.map(async (item) => {
var itemDescription = await productsModel.findOne({id: item.itemId})
return itemDescription;
}));

.map() is not promise-aware. It doesn't pay any attention to the promise that your async callback function returns. So, as soon as you hit the await productsModel.findOne(...), that async function returns an unfulfilled promise and the .map() advances to the next iteration of the loop.
There are many different ways to solve this. If you want to use .map(), then you need to pay attention to the promise that your callback is returned like this:
app.get('/cart', checkAuthenticated, async (req, res) => {
if (req.user.cart.length > 0) {
req.user.items = await Promise.all(req.user.cart.map((item) => {
return productsModel.findOne({ id: item.itemId });
}));
console.log(req.user)
}
});
The above implementation will attempt to run all the database lookups in parallel.
A somewhat simpler implementation just uses a plain for loop and runs the database lookups one at a time:
app.get('/cart', checkAuthenticated, async (req, res) => {
if (req.user.cart.length > 0) {
req.user.items = [];
for (let item of req.user.cart) {
req.user.items.push(await productsModel.findOne({ id: item.itemId }));
}
console.log(req.user)
}
});

In your example, the array is still empty because the callback in the map function works asynchronously, hence you need to wait while the code will complete. Because map function returns array of promises, they all need to be settled using Promise.all:
app.get('/cart', checkAuthenticated, async (req, res) => {
if (req.user.cart.length > 0) {
req.user.items = []
const promises = req.user.cart.map(async (item) => {
var itemDescription = await productsModel.findOne({ id: item.itemId })
req.user.items.push(itemDescription)
});
await Promise.all(promises);
console.log(req.user)
}
});
Otherwise, you can replace map function with for loop:
app.get('/cart', checkAuthenticated, async (req, res) => {
if (req.user.cart.length > 0) {
req.user.items = []
for (const item of req.user.cart) {
var itemDescription = await productsModel.findOne({ id: item.itemId })
req.user.items.push(itemDescription)
}
console.log(req.user)
}
});

Related

array.push method not working for my node.js application [duplicate]

This question already has answers here:
How to return many Promises and wait for them all before doing other stuff
(6 answers)
Closed 11 months ago.
router.get("/timeline/:userId", async (req, res) => {
try {
//creating friend posts object
const friendPosts = await Promise.all(
currentUser.followings.map((friendId) => {
return Post.find({ userId: friendId });
})
);
var newFriendPosts = [];
friendPosts.map(async (friend) => {
friend.map(async (post) => {
const { profilePicture, username } = await User.findById(post.userId);
const newFriendPostsObject = {
...post.toObject(),
profilePicture,
username,
};
console.log(newFriendPostsObject);
newFriendPosts.push(newFriendPostsObject);
});
});
console.log(newFriendPosts);
// console.log(newUserPosts);
res.status(200).json(newUserPosts.concat(...newFriendPosts));
} catch (err) {
res.status(500).json(err);
}
});
So the object value is coming correct in console.log(newFriendPostsObject) but when I push that in the array newFriendPosts = []; it shows empty array in console.log(newFriendPosts);
Below is where I am getting user details from DB (DB is in MongoDB):-
const { profilePicture, username } = await User.findById(post.userId);
.map() is NOT async aware. So, it just marches on and returns an array of promises. Meanwhile, your code never waits for those promises so you're trying to use newFriendPosts BEFORE any of your .push() operations have run. .push() is working just fine - you're just trying to use the array before anything has actually gotten around to calling .push(). If you add appropriate console.log() statements, you will see that the newFriendPosts.push(...) occurs after your console.log(newFriendPosts);.
The simplest fix is to change both .map() statements to a regular for loop because a for loop IS async aware and will pause the loop for the await.
router.get("/timeline/:userId", async (req, res) => {
try {
//creating friend posts object
const friendPosts = await Promise.all(
currentUser.followings.map((friendId) => {
return Post.find({ userId: friendId });
})
);
const newFriendPosts = [];
for (let friend of friendPosts) {
for (let post of friend) {
const { profilePicture, username } = await User.findById(post.userId);
const newFriendPostsObject = {
...post.toObject(),
profilePicture,
username,
};
newFriendPosts.push(newFriendPostsObject);
}
}
console.log(newFriendPosts);
res.status(200).json(newUserPosts.concat(...newFriendPosts));
} catch (err) {
res.status(500).json(err);
}
});
One mystery in this code is that newUserPosts does not appear to be defined anywhere.

Why does my async function return an empty array?

I'm trying to get the matches of my user by pushing them in an array and returning this array, so my router can send the data to the front-end. But I've got an issue with my async function: I just got an empty array. I've tried to put some breakpoints, and I noticed that my router sends the data before my service pushes the data to the array.
Here is my router code:
router.get("/allMatchs", auth, async (req, res) => {
const user = await userService.getUserById(req);
const matchs = await service.getMatchsByUser(user);
res.send(matchs);
});
and there is my service code:
async function getMatchsByUser(user) {
const userMatchs = user.matchs;
let matchs;
await userMatchs.map(async (m) => {
let match = await Match.findById(m._id).select([
"-isConfirmed",
"-isUnmatched",
]);
matchs.push(match);
});
return matchs;
}
Thank you for your help.
It's because .map() is not async aware. It doesn't wait for the promise that the callback returns. So, when you do this:
await userMatchs.map(...)
The .map() returns an array. You are calling await on an array of promises (remember, .map() returns an array). That doesn't do anything useful. It doesn't wait for anything and the individual iterations inside the .map() didn't wait either.
You can either switch to plain for loop because a forloop is promise aware and it will await properly or you can use await Promise.all(userMatchs.map(...)) .
You could do this:
function getMatchsByUser(user) {
return Promise.all(user.matchs.map((m) => {
return Match.findById(m._id).select([
"-isConfirmed",
"-isUnmatched",
]));
});
}
Or, if you want to do the requests to your database sequentially one at a time, use a plain for loop which await will work in:
async function getMatchsByUser(user) {
let matchs = [];
for (let m of user.matchs) {
let match = await Match.findById(m._id).select([
"-isConfirmed",
"-isUnmatched",
]);
matchs.push(match);
}
return matchs;
}
This code
userMatchs.map(async (m) => {
let match = await Match.findById(m._id).select([
"-isConfirmed",
"-isUnmatched",
]);
matchs.push(match);
}); // -> return an array of promises
It returns an array of promises, so await will not wait for that to execute, -> return empty array.
For this, you should use Promise.all()
Something like this:
async function getMatchsByUser(user) {
const userMatchs = user.matchs;
let matches = await Promise.all(userMatchs.map(async (m) => {
return Match.findById(m._id).select([
"-isConfirmed",
"-isUnmatched",
]);
}));
return matches;
}
Your problem is userMatchs.map
Using async/await combined with map() can be a little tricky
How to use Async and Await with Array.prototype.map()

How to wait for MongoDB results in multiple nested loops

I am trying to load a certain page from my website only after the data is loaded from the database. However, it prints the results too early, giving an empty array, and it doesn't group the names at all. I believe this may be solved with async and await functions or callbacks, but I'm new to those and despite what I've searched, I still can't find the solution. So far, I've just been messing around with wrapping certain loops or functions in async functions and awaiting them. Other solutions have been focused on one .find() or other loop. Any help is appreciated! Thank you!
Routes.js:
app.get("/test", async (req, res) => {
var result = [];
users.findById(mongo.ObjectID("IDString"), async (err, res) => {
result = await res.getGroups(users, messages);
});
console.log(result);
//this should print [["name1"]["name1", "name2"],["name3", "name1"]]
res.redirect("/");
});
getGroups:
UserSchema.methods.getGroups = async function(users, messages) {
var result = [];
await messages.find({Group: this._id}, (err, group) => {
for(var i = 0; i < group.length; i++){
var members = group[i].Group;
var subArray = [];
members.forEach((memberID) => {
users.findById(memberID, (err, req) => {
console.log(req.name);
subArray.push(req.name);
console.log(subArray);
});
});
//After function finishes:
console.log(subArray);
result.push(subArray);
subArray = [];
}
});
return result;
};
In your first code snippet, you must set
console.log(result);
//this should print [["name1"]["name1", "name2"],["name3", "name1"]]
res.redirect("/");
in the callback function of findById. Like this:
app.get("/test", async (req, res) => {
var result = [];
users.findById(mongo.ObjectID("IDString"), async (err, res) => {
result = await res.getGroups(users, messages);
console.log(result);
//this should print [["name1"]["name1", "name2"],["name3", "name1"]]
res.redirect("/");
});
});
This is because of asynchronous nature of Javascript. Read the article below
https://medium.com/better-programming/is-javascript-synchronous-or-asynchronous-what-the-hell-is-a-promise-7aa9dd8f3bfb
If you want to use async/await paradigm, you can write this code like this:
app.get("/test", async (req, res) => {
var result = [];
result = await users.findById(mongo.ObjectID("IDString"));
console.log(result);
//this should print [["name1"]["name1", "name2"],["name3", "name1"]]
res.redirect("/");
});
In your second code snippet, you should use await OR callback. Just as explained above.

Javascript promise chain hell

I'm having a problem with promise chains that I don't know how to resolve. Summary of my code: I do a mongoose query for a specific user, fetch his CarIds and then query each car for his details and return these details via JSON response.
let carsDetails = [];
User.findById(userId)
.then(user => {
const carIds = user.carsDetails;
carIds.forEach((carId) => {
Car.findById(carId)
.then(car => {
console.log(car);
carsDetails.push(car);
})
.catch(err => { throw err; });
});
return res.status(200).json({ data: carsDetails });
})
.catch(error => {
throw error;
});
The problem is that no cars get actually pushed onto carsDetails array on the carsDetails.push(car); line, because it jumps to return statement before it manages to fill up the array. Is there a workaround that could do those queries and return a result in a form of an array, object...? I tried writing everything in async await form too with self-made async forEach statement, but it doesn't help me. Any suggestions? I already tried with Promise.all(), but didn't manage to fix the issue. Thanks!
You'll need to collect the promises of your Car.findById(carId) calls and use Promise.all() to wait for all of them before responding. You can use array.map() to map each ID to a promise from Car.findById().
User.findById(userId)
.then(user => {
const carIds = user.carsDetails;
const carPromises = carIds.map(carId => Car.findById(carId))
return Promise.all(carPromises)
})
.then(cars => {
res.status(200).json({ data: cars })
})
.catch(error => {
throw error
})
If you can have a lot of cars to find, you may want to do your query in a single request, no need to stack multiple promises :
User.findById(userId)
.then(user => {
const carIds = user.carsDetails;
// if carsDetails is not an array of objectIds do this instead :
// const carIds = user.carsDetails.map(id => mongoose.Types.ObjectId(id));
return Car.find({ _id: { $in: carIds });
})
.then(userCars => {
res.status(200).json({ data: userCars })
})
await/async is the way to go, with await/async you use regular for ... of loops instead of forEach.
async function getCarDetails() {
let carsDetails = [];
let user = await User.findById(userId);
const carIds = user.carsDetails;
for (let carID of carIds) {
let car = await Car.findById(carId)
console.log(car);
carsDetails.push(car);
}
return res.status(200).json({
data: carsDetails
});
}
Or you use Promise.all and map instead of for ... of
async function getCarDetails() {
let user = await User.findById(userId);
const carIds = user.carsDetails;
let carsDetails = await Promise.all(carIds.map(carID => Car.findById(carID)));
return res.status(200).json({
data: carsDetails
});
}
Those two solutions are slightly different. The second version with the map will send all requests to the DB at once, and then waits until they are all resolved. The first one will send the request one after another. Which one is better depends on the use-case, the second one could lead to request peeks, and might be easier be abused for DDoS.
Try to use async/await to solve this problem. It will be more readable and clean
async function getCarDetail() {
let carsDetails = []
try {
const user = await User.findById(userId)
const carIds = user.carsDetails
for (let i = 0; i < carIds.length; i++) {
const carId = carIds[i]
const car = await Car.findById(carId)
carsDetails.push(car)
}
return res.status(200).json({ data: carsDetails })
} catch(error) {
console.log(error)
}
}

Wait for multiple database calls

Having trouble wrapping a loop in a promise
I need to make an array of users before i render my page but i cant seem to figure out how to wrap my database calls
Thanks in advance.
router.get('/friends', auth.isLogged(), (req, res) => {
let friendsList = [];
User.findById(req.user._id,
{
friends: 1,
},
(err, user) => {
user.friends.map(friend => {
User.findById(friend._id).then(doc => {
friendsList.push(doc);
});
});
console.log(friendsList); <-- gets called before the loop is done
});
});
Turn the friends into an array of promises, and await a Promise.all over all of them, like this:
router.get('/friends', auth.isLogged(), (req, res) => {
let friendsList = [];
User.findById(req.user._id, {
friends: 1,
},
async (err, user) => {
const friendsList = await Promise.all(
user.friends.map(({ _id }) => User.findById(_id))
);
console.log(friendsList);
}
);
});
wrap it in Promise.all():
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
Promise.all() doesn't resolve until all the promises are resolved or one i rejected.

Categories

Resources