Async/Await with map or foreach - javascript

I'm trying to retrieve a bunch of product prices from my database and assumed I could just map, or foreach, through them and += the price onto a variable as below:
// Get Total
exports.getTotal = (req,res) => {
let productList = req.body;
let total = 0;
const results = productList.map(async (product) => {
Product.findById(product._id)
.select('price')
.exec((err, foundProduct) => {
if (err){
console.log(`Error: Product with id ${product._id} not found.`);
} else {
console.log('Product price. ', foundProduct.price);
total += foundProduct;
}
})
});
Promise.all(results).then(data => console.log('Total is', total));
};
However the console.log for the total always returns 0. I suspect it's an issue that the console.log is running before the map and database lookup promise has complete.
Any guidance appreciated.

You are using exec in a wrong way Exec returns you a promise. You can simplify this like
// Get Total
exports.getTotal = (req, res) => {
const productList = req.body;
let total = 0;
const results = productList.map(async product => {
const foundProduct = await Product.findById(product._id).exec();
total += foundProduct;
return foundProduct;
});
Promise.all(results).then(data => console.log("Total is", total));
};

you forgot to await an asynchronous function await Product.findById(product._id).
Also writing in a functional way removes clutter for this kind of task by using array reduce.
// Get Total
exports.getTotal = (req, res) => {
const results = req.body.reduce(async (total, product) => {
await Product.findById(product._id)
.select('price')
.exec((err, foundProduct) => {
if (err){
console.log(`Error: Product with id ${product._id} not found.`);
return total
} else {
console.log('Product price. ', foundProduct.price);
return total += foundProduct;
}
})
}, 0);
Promise.all(results).then(data => console.log('Total is', data));
};

Array's map(), foreach() and other iteration methods do not use promises. So making the callbacks async does not make the internal iteration wait before the doing the next iteration call to the callback. Also your map would have been filled with promises that never get resolved as you never return anything, it would be equivalent to having
Promises.all([new Promise(), new Promise(), new Promise()])
You can change your code to create a proper map of new promises and then calculate the total after everything has been fetched. Note since exec() itself returns a promise you just return that:
const results = productList.map(product=> {
return Product.findById(product._id).select('price').exec();
});
Promise.all(results).then(values => {
let total = values.reduce((a, b) => a + b);
console.log(`Total: ${total}`)
});
You could also just use a for loop in a standalone async function and use await
async function getTotal(){
let total = 0;
for(let product of productList){
let price = await Product.findById(product._id).select('price').exec();
total+=price;
}
return total;
}
getTotal().then(total=>console.log(`Total: ${total}`));

Related

Async/Await in javascript for loop

I have a react component that runs this function on the mounting of the component.
function getListOfItems(){
let result = [];
for(let i=0 ; i<5 ; i++){
/**
* code to assign values to some variables namely param1,param2
*/
getDetails(param1,param2);
}
const getDetails = async (param1,param2) => {
let list = await getAPIresults(param1)
result.push(list);
if(result.length === 5){
//code to update a hook which causes render and displays the text in results array
}
}
}
useEffect(() => {
getListOfItems()
},[])
So the code is running but the results array has data in random order. For instance, the results array might look something like this [2,5,1,3,4] where I expect it to be like this [1,2,3,4,5] This means the above code is not running async tasks in the order of their arrival. So could someone help me out to fix this, I want the code to make async requests in order of their arrival.
You need to use the await keyword again to wait for each iteration of the loop to complete before it moves on to the next go-round.
await getDetails(param1,param2)
But as you can only do this in an async function, your getListOfItems will also need to be an async function.
async function getListOfItems(){
let result = [];
for(let i=0 ; i<5 ; i++){
await getDetails(param1,param2);
}
const getDetails = async (param1,param2) => {
let list = await getAPIresults(param1)
result.push(list);
if(result.length === 5){}
}
}
So the code is running but the results array has data in random order.
That's because your loop calls getDetails repeatedly without waiting for the previous call to complete. So all the calls overlap and race.
If it's okay that they overlap but you need the results in order, use Promise.all and have getDetails return its results (rather than pushing them directly).
If you can't make getListOfItems an async function:
const getDetails = async (param1,param2) => {
let list = await getAPIresults(param1)
if(result.length === 5){
//code to update a hook which causes render and displays the text in results array
}
return list;
}
const promises = [];
for (let i = 0; i < 5; ++i) {
promises.push(getDetails(param1, param2));
}
Promise.all(promises)
.then(results => {
// `results` is an array of the results, in the same order as the
// array of promises
})
.catch(error => {
// Handle/report error
});
If you can (and the caller will handle any error that's propagated to it via rejection of the promise from getListOfItems):
const getDetails = async (param1,param2) => {
let list = await getAPIresults(param1)
if(result.length === 5){
//code to update a hook which causes render and displays the text in results array
}
return list;
}
const promises = [];
for (let i = 0; i < 5; ++i) {
promises.push(getDetails(param1, param2));
}
const results = await Promise.all(promises)
// `results` is an array of the results, in the same order as the
// array of promises
If you need them not to overlap but instead to run one after another, your best bet is to use an async function for the loop.
If you can't make getListOfItems an async function:
const getAllResults = async function() {
const results = [];
for (let i = 0; i < 5; ++i) {
results.push(await getDetails(param1, param2));
}
return results;
}
const getDetails = async (param1,param2) => {
let list = await getAPIresults(param1)
if(result.length === 5){
//code to update a hook which causes render and displays the text in results array
}
return list;
}
getAllResults()
.then(results => {
// `results` is an array of the results, in order
})
.catch(error => {
// Handle/report error
});
If you can (and the caller will handle any error that's propagated to it via rejection of the promise from getListOfItems):
const results = [];
for (let i = 0; i < 5; ++i) {
results.push(await getDetails(param1, param2));
}
// Use `results
const getDetails = async (param1,param2) => {
let list = await getAPIresults(param1)
if(result.length === 5){
//code to update a hook which causes render and displays the text in results array
}
return list;
}
You might want to use Promise.all; this would preserve the order as well:
Promise.all: Order of resolved values

How to return array at end of loop (Newbie question)

The following code (which I've simplified for clarity) loops through and returns the cardsToInsert before each part of the loop finishes, so the array doesn't get built properly.
The loops build the array correctly, but the result got returned near the beginning, not after it was built.
How do I get it to finish all the loops before returning the array?
async function loopCards(cardsToGet) {
for (let cardToGet of cardsToGet) {
getDataFunctionWhichReturnsAPromise()
.then(async (card) => {
writeCard = await buildingCard(card)
cardsToInsert.push(writeCard)
}
return cardsToInsert
}
thanks
Full Code Added as Requested
// wixData.get() returns a promise
async function loopCards(cardsToGet) {
let writeCard
let buildCard
for (let index = 0; index < cardsToGet.length; index++) {
const cardToGet = cardsToGet[index].card
buildCard = wixData.get("Card", cardToGet)
.then(async (card) => {
writeCard = await buildingCard(card)
cardsToInsert.push(writeCard)
}
)
.catch((err) => {
let errorMsg = err;
console.log("getcard error: " + errorMsg);
return errorMsg
});
}
return cardsToInsert
}
Here is a detailed explaination
loops are synchronous.
Promises are asynchronous.
To get the data from promises you need to wait for it to finish using callback,async-await or promise.
In your code, you are putting .then to access the result of wixData but the whole wixData.get("Card", cardToGet).then(async (card) => {}) is an async function for loopCards(cardsToGet) and because of this your loop finishes and the result array is empty.
Solution -
Just wait for wixData.get("Card", cardToGet) to finish and then do the manipulations. Here is a solution using async await.
async function loopCards(cardsToGet) {
let cardsToInsert = [];
for (let index = 0; index < cardsToGet.length; index++) {
const cardToGet = cardsToGet[index].card
let card = await wixData.get("Card", cardToGet)
let writeCard = await buildingCard(card)
cardsToInsert.push(writeCard)
}
return cardsToInsert
}
In the above code I wait for the wixData.get("Card", cardToGet) to finish and store the result in get card. This should fix your problem.
but this code by performance is not optimal as you are waiting for each network call. what you can do is execute all the Promises at once using Promise.all
Here is the code for that with error handling -
async function loopCards(cardsToGet){
try {
return await Promise.all( cardsToGet.map( cardToGet => buildCard( cardToGet.card )));
}
catch (error) {
console.log(error);
}
}
async function buildCard(cardToGet){
try {
let card = await wixData.get("Card", cardToGet);
let writeCard = await buildingCard(card);
return writeCard;
}
catch (error) {
console.log("getcard error: " + error);
return error;
}
}
The above code might have some erros since I haven't tested it but I hope you get the approach.
you should wrap loop with in promise .then or also you are using async its mean a very first item of array will be print and then it will await
getDataFunctionWhichReturnsAPromise()
.then(async (card) => {
for (let cardToGet of cardsToGet) {
writeCard = await buildingCard(card)
cardsToInsert.push(writeCard)}
}

Async functions in for loops javascript

I am not experienced with async functions and I would like to perform a request in a for loop. Here's my code:
app.route('/friendlist').post((req, res) => {
var body = req.body;
var list = "";
con.query(`SELECT * FROM player_friends WHERE main_user_id = '${body.player_id}'`, (err, row, fields) => {
if (err) throw err;
async function queryOutUserData(data) {
var rows = await new Promise((resolve, reject) => {
con.query(`SELECT * FROM players WHERE player_id = '${data.player_id}'`, (error, player, field) => {
if (error) {
console.log(error);
return reject(error);
}
resolve(player);
});
});
rows.then(message => {
return message
});
}
for (var i = 0; i <= row.length; i++) {
console.log(row[i].main_user_id);
var result = await queryOutUserData(row[i]);
list = list + ";" + result[0].player_id + ":" + result[0].player_username;
}
console.log(list);
return list;
});
});
Actually here's the full problem: I did some debugging and apparently value i in for loop increases before the promise is resolved. Also as I mentioned I am not familiar with async functions, could you provide me a descriptive resource about how promises and async functions work?
Thanks
NOTE: For better indentation, here's the code: https://hastebin.com/fovayucodi.js
Instead of using async/await I suggest doing everything in one query using WHERE IN rather than one query per player. See if the following fits your needs:
app.route('/friendlist').post((req,res) => {
var body = req.body;
var list = "";
con.query(`SELECT * FROM player_friends WHERE main_user_id = '${body.player_id}'`, (err, row, fields) => {
if (err) throw err;
const playerIds = row.map(player => player.player_id);
con.query(`SELECT * FROM players WHERE player_id IN ${playerIds}`, (error, players, field) => {
for (let player of players) {
list += `;${player.player_id}:${player.player_username}`;
}
});
console.log(list);
return list;
});
});
If you await a promise, it evaluates to the result of that promise, so rows is not a promise, it's the result. So this:
rows.then(message => {return message});
Doesn't make much sense, just do:
return message;
Also, you have an await inside of a regular function, thats a syntax error.
Additionally return list; doesn't do much (if that is express), you might want to return res.json({ list });.
: I did some debugging and apparently value i in for loop increases before the promise is resolved.
I doubt that you can debug code if you can't actually run it because of the syntax errors.
try to use for-of instead just a for.
something like this:
Async Function:
async function test() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(true);
return
}, 1000)
})
}
Here another function using for and waiting for the finish of loop
async function myFunction() {
const data = [1,2,3,4,5,6,7,8]
for(let i of data) {
const value = await test();
console.log(value)
}
console.log("finish");
}

For loop total increments in loop, but when printing afterwards returns 0

I'm creating an Alexa application that acts as a portfolio manager for stocks. I have a function which first retrieves all the stocks and the amount of stocks belonging to a user. For each of these stocks, I must query an API using axios to get the value of the stock, meaning the axios get statement is nested within the forEach loop. I then have a totalPrice variable, which is incremented using the total price of a stock; which is found by multiplying the price by the amount of stock x owned
At the end of the forEach loop I want to print the totalPrice which would be the whole value of a portfolio
When I print out the totalPrice within the request statement, it correctly adds previous total stock prices, but if I print the totalPrice after the forEach loop, it prints 0
var grandTotal = 0;
data.Items.forEach(function(element,index,array) {
var stock = element.Stock;
var number = element.Number;
var url = `www.api.com/${stock}`;
const getDataValues = async url => {
try {
const response = await axios.get(url);
const data = response.data;
var Price = data.PRICE;
return Price;
} catch (error) {
console.log(error);
}
};
let promiseObject = getDataValues(url);
promiseObject.then(function(result) {
var totalPriceofStocks = result * amount;
grandTotal += totalPriceOfStocks;
console.log(`{grandTotal}`); // This bit accumulates correctly
});
});
console.log(`The grand total is: ${grandTotal}`);
what's confusing me is that although the data is asynchronous, I thought that using .then would wait until the data is retrieved. This seems to be the case as while printing the total in the forEach loop this works. Something else which is interesting is that in the console, "The grand total is: 0" is printing first.
The console.log in the end prints 0 because it is executed immediately after your promises are initiated (and not finished yet).
In order to wait until all the promises have finished, then you can:
Store the promises in a list
Use Promise.all, which takes a list of promises as parameter, and resolves when all promises have resolved (in your case, when all axios requests are finished).
I have added a modified example of your code, to show how to do this. As I do not have your data I have not been able to test it, but hopefully it works.
const promises = []
const getDataValues = async url => {
try {
const response = await axios.get(url);
const data = response.data;
var Price = data.PRICE;
return Price;
} catch (error) {
console.log(error);
}
};
var grandTotal = 0;
data.Items.forEach(function(element,index,array) {
var stock = element.Stock;
var number = element.Number;
var url = `www.api.com/${stock}`;
let promiseObject = getDataValues(url);
promiseObject.then(function(result) {
var totalPriceofStocks = result * amount;
grandTotal += totalPriceOfStocks;
console.log(`${grandTotal}`); // This bit accumulates correctly
return totalPriceOfStocks
});
promises.push(promiseObject)
});
Promise.all(promises)
.then(function (resultValues) {
console.log(`The grand total is: ${grandTotal}`);
let resultSum = 0;
resultValues.forEach(function (value) {
resultSum += value;
})
console.log(`This is also the total: ${resultSum}`);
})
Notice also that I have added a return value to your promise object. I have this to show an alternative way of accumulating the grand total rather than relying on a global value.
The logic behind this, is that when a Promise returns a value, that value is made available for the function passed to the 'then' chained function. In this case, where we use Promise.all, Promise.all passes a list of the promise's return values to its 'then' function. Therefore, we can add the elements of the returnValues parameter together to compute the grand total.
Use for..of loop instead of forEach in which await should work as expected
for (const element of data.Items) {
var stock = element.Stock;
var number = element.Number;
var url = `www.api.com/${stock}`;
const getDataValues = async url => {
try {
const response = await axios.get(url);
const data = response.data;
var Price = data.PRICE;
return Price;
} catch (error) {
console.log(error);
}
};
const result = await getDataValues(url);
var totalPriceofStocks = result * amount;
grandTotal += totalPriceOfStocks;
console.log(`{grandTotal}`);
}

Push into an array from foreach and make it available outside foreach

I stuck by looping through an array that receive values from a promise and push values into a new array which is available outside the foreach.
What i have:
app.post('/submit', function (req, res) {
uploadPics(req, res, function (err) {
if (err instanceof multer.MulterError) {
res.send(JSON.stringify({UploadResult: err.message}));
console.log(err.message + ' ' +'Redirect /home');
} else if (err) {
console.log(err);
} else {
res.send(JSON.stringify({UploadResult: 'Success'}));
var filesarray = req.files;
var picinfos = [];
filesarray.forEach(function(file){
GetFileMetaInfo.filemetainfo(file.path).then(function (metadata){
//Stuck here! Can push values into an array (picinfos) but only available in the foreach. not outside..
})
})
//I need picinfos array here....
}
})
})
How i receive my metadata:
var exif = require('exif-parser');
var fs = require('fs');
exports.filemetainfo = function (filepath) {
return new Promise((resolve) => {
var file = filepath;
var buffer = fs.readFileSync(file);
var parser = exif.create(buffer);
var result = parser.parse();
resolve (result);
}).then(function (metadata){
if (metadata.tags.CreateDate !== undefined){
date = new Date (metadata.tags.CreateDate*1000);
datevalues = [
date.getFullYear(),
date.getMonth()+1,
date.getDate(),
date.getHours(),
date.getMinutes(),
date.getSeconds(),
];
CreateDate = date.getFullYear()+'-'+(date.getMonth()+1)+'-'+date.getDate();
CreateTime = date.getHours()+':'+date.getMinutes()+':'+date.getSeconds();
console.log("CrDate:" +CreateDate, "CrTime:" +CreateTime );
} else {
console.log("No Metadata Creation Infos found in " +filepath);
CreateDate = "";
CretaeTime = "";
}
if (metadata.tags.GPSLatitude !== undefined){
GPSLat = metadata.tags.GPSLatitude;
GPSLon = metadata.tags.GPSLongitude;
console.log("GPSLat:" + GPSLat , "GPSLon:" +GPSLon);
}
else {
console.log("No Metadata GPS Infos found in " +filepath)
GPSLat = "";
GPSLon = "";
}
return MetaData = {
GPSLat: GPSLat ,
GPSLon: GPSLon,
CreateDate: CreateDate,
CreateTime: CreateTime,
}
})
}
May i ask someone to give a hand. How can i make my array available outside the foreach. thank you very much!
The reason you're getting empty array at the end of forEach is because, GetFileMetaInfo.filemetainfo() returns a promise and forEach won't wait for async actions.
You could use async/await with for...of loop to get your desired result.
app.post('/submit', function (req, res) {
uploadPics(req, res, async function (err) { // note async here
if (err instanceof multer.MulterError) {
res.send(JSON.stringify({UploadResult: err.message}));
console.log(err.message + ' ' +'Redirect /home');
} else if (err) {
console.log(err);
} else {
res.send(JSON.stringify({UploadResult: 'Success'}));
var filesarray = req.files;
var picinfos = [];
for(let file of filesarray) {
const metadata = await GetFileMetaInfo.filemetainfo(file.path);
// push metadata into your array here
picinfos.push(metadata);
}
// You will have picinfos here
}
})
})
Although the question is already answered by Dinesh Pandiyan there are still some adjustments that can be made. The following code in his answer runs sequential, meaning that every async request is made after the previously returned result is resolved.
for(let file of filesarray) {
const metadata = await GetFileMetaInfo.filemetainfo(file.path);
// ^- pauses the execution of the current running code
// push metadata into your array here
picinfos.push(metadata);
}
async call #1 ╌╌await╌╌> async call #2 ╌╌await╌╌> async call #3 ╌╌await╌╌> result
You could make the code concurrent by first executing all async statements and then wait until all results are resolved. This can be done by simply changing the following:
// execute all the async functions first, reducing the wait time
for(let file of filesarray) {
const metadata = GetFileMetaInfo.filemetainfo(file.path);
// ^- remove the await
// push metadata into your array here
picinfos.push(metadata);
}
// wait for all results to be resolved
picinfos = await Promise.all(picinfos);
// ^- instead await here
async call #1 ╌╌┐
async call #2 ╌╌┼╌╌await all╌╌> result
async call #3 ╌╌┘
The above could be further simplified by simply using an Array.map() in combination with the already shown Promise.all().
var filesarray = req.files;
var picinfos = await Promise.all(filesarray.map(file => {
return GetFileMetaInfo.filemetainfo(file.path);
}));
// picinfos should be present
Or if you want to avoid working with async/await:
var filesarray = req.files;
Promise.all(filesarray.map(file => {
return GetFileMetaInfo.filemetainfo(file.path);
})).then(picinfos => {
// picinfos should be present
});

Categories

Resources