I am making a function call 3 times with different arguments:
this.getContributorProperties('followers_url', 'contributorFollowers');
this.getContributorProperties('gists_url', 'contributorGists');
this.getContributorProperties('repos_url', 'contributorRepositories');
This function looks like that:
async getContributorProperties(propertyUrl, propertyName) {
const contributors = await this.addLinkToContributor();
for (let i = 0; i < 10; i += 1) {
axios.get(`${contributors[i][propertyUrl]}?per_page=100&${API_KEY}`).then((res) => {
contributors[i][propertyName] = res.data.length;
});
}
return contributors;
}
It loops through an array of contributors (object type) and makes an API call for each one of them. I need to make 3 API calls for each one of them hence the three calls at the beginning.
In order to DRY up my code I wanted to make a forEach loop like so:
[
['followers_url', 'contributorFollowers'],
['gists_url', 'contributorGists'],
['repos_url', 'contributorRepositories'],
].forEach(this.getContributorProperties);
forEach loop is in componentDidMount()
When I make 3 calls it works ok. But when I do forEach I get an error:
Uncaught (in promise) TypeError: Cannot read property 'addLinkToContributor' of undefined
How do I make it work?
BONUS: How do I then assign those key-value pairs to each object?
See How to access the correct this inside a callback? and/or How does the "this" keyword work? for why you had that specific error message.
But fundamentally, you wouldn't want to just pass that function into forEach anyway, since forEach doesn't pass that function what it wants.
Instead, just use an arrow function:
[
['followers_url', 'contributorFollowers'],
['gists_url', 'contributorGists'],
['repos_url', 'contributorRepositories'],
].forEach(pair => this.getContributorProperties(pair[0], pair[1]).catch(err => {/*...handle error...*/});
Note the error handling; we don't want unhandled rejections, and forEach doesn't do anything to propagate them to the caller.
It seems odd, though, not to use the return value for anything. Perhaps map and Promise.all:
const results = await Promise.all([
['followers_url', 'contributorFollowers'],
['gists_url', 'contributorGists'],
['repos_url', 'contributorRepositories'],
].map(pair => this.getContributorProperties(pair[0], pair[1])));
...being sure to handle errors if the caller won't.
Please note there are two bugs in your getContributorProperties function:
It doesn't wait for the axios.get to complete before returning (async functions don't auto-await promises, you have to be explicit)
It doesn't handle rejections of the promise returned by axios.get
I'm also curious if repeating the call to this.addLinkToContributor three times is correct, and whether it may be wasteful the second two times.
In a comment you've asked:
Results are 3 arrays of the same objects (contributors) with just one property changed each. So one array has contributors with gists, another with followers, etc. Do I now somehow concatenate them or is it better to do that in getContributorProperties function?
That would be my instinct. Something like:
async getContributorProperties(properties) {
const contributors = await this.addLinkToContributor();
return Promise.all(contributors.map(contributor =>
Promise.all(properties.map(property =>
axios.get(`${contributor[property.url]}?per_page=100&${API_KEY}`).then(res => {
contributor[property.name] = res.data.length;
})
));
));
}
called like this:
const results = await this.getContributorProperties([
{url: 'followers_url', name: 'contributorFollowers'},
{url: 'gists_url', name: 'contributorGists'},
{url: 'repos_url', name: 'contributorRepositories'}
]);
(We really need that await.all concept so that the above isn't mixing its metaphors...)
Related
This question already has answers here:
Using async/await with a forEach loop
(33 answers)
Closed 1 year ago.
I was wondering what is the problem here, why is the return statement not waiting to finish the previous instruction? orders are empty, even though I'm logging inside the getOrders to make sure there were data (there is data).
await stores.forEach(async (store) => {
let arr = await getOrders(store, token, tokenType);
orders.push(arr);
})
return orders;
To wait for all promises, you need an array of promises, which you can get using map. Then you need to use Promise.all, so that it will wait for all the promises in that array to complete.
const orders = await Promise.all(
stores.map(store => getOrders(store, token, tokenType))
);
return orders;
*Edit: rewritten and simplified based on suggestion from #PatrickRoberts
To paraphrase his comment, in the old code when using a separate array with an array.push, the array ordering would be indeterminate. However using the array of results from Promise.all will always match the order of the original array.
It's a common misunderstanding that asynchronous callbacks (declared with async and called with await) passed to forEach or map will be executed synchronously or in order. Actually, they are not.
When you check the polyfill of those methods, you'll find a line like this callback.call(T, kValue, k, O);. So, basically, it just execute the callback. If the callback is an asynchronous method, it doesn't wait its execution to be done. Instead, it continue executing other codes.
Let's set an example using your code, let' say this is the callback:
const callback = async (store) => {
let arr = await getOrders(store, token, tokenType);
orders.push(arr);
})
When it was passed to forEach:
stores.forEach(callback);
basically, what forEach does is iterating the array, and call callback on each elements. May looks like this
...
loop {
callback.call(...)
}
...
There is no wait here. However, each time, the code inside your callback is executed synchronously since it's declared with async and called with await, which means after getOrders is resolved and the result is pushed to orders.
So, if you want to execute the callbacks synchronously and in order, you may write a generic for loop.
async function addOrders(){
for(let store of stores) {
let arr = await getOrders(store, token, tokenType);
orders.push(arr);
}
}
addOrders()
In JavaScript, I'm calling a promise which returns an id; we'll call this promise1. promise1 has a following .then() which, given the id returned from promise1, calls a for-each loop. On each iteration of this loop, promise2 is carried out
promsie2 also has a .then(). so the data returned from promise2 can be utilized.
mycode.js
exports.my_function = (req, res) => {
var data = req.body;
var name_array = ["Jeff", "Sophie", "Kristen"]
var personal_ID_arr = []
promise1.getID(data) //promise1 gets an int 'id'
.then(id => {
array.forEach(name => { //for each name
promise2.getPersonalID(name, id) //promise2 creates a personal id for each person using their name and the id generated from promise1
.then(personalID => {
personal_ID_arr.push(personalID) //add the generated personal id to an arr
})
})
})
//will need to operate on 'personal_ID_arr' here after the above finishes or possibly still within the promise1's .then; as long as the arr is filled fully
}
However, I'm running into the issue of synchronicity not allowing for this type of logic to happen, as things begin happening out of order once it gets to the loop aspect of the program. I need to carry out another function with the filled-out 'personal_ID_arr' after these promises are carried out as well
I've looked at other questions regarding promise chaining but this is a different case due to the fact the for loop needs to work with the data from the first promise and use .then() inside a .then(). Surely, I don't need to keep this structure though if a better one exists.
Any help would be greatly appreciated
Keeping an array of IDs that an asynchronous operation will fill, is not something that will help you carrying out more asynchronous operations on the array later. Instead, consider using an array of Promises for each name, and wait them all to resolve using Promise.all. Also, i removed the personal_ID_arr from the function my_function variables and moved it to the appropriate asynchronous operation. I did a simple console log of the array, you should fill your next step there.
Although maybe you need to write this logic with Promises, but I would suggest using async / await for this task, it will lead to more readable code.
exports.my_function = (req, res) => {
var data = req.body;
var name_array = ["Jeff", "Sophie", "Kristen"]
promise1.getID(data) //promise1 gets an int 'id'
.then(id =>
//map the array of Strings to array of Promises instead, wait for them to resolve
Promise.all(name_array.map(name =>
promise2.getPersonalID(name, id)
))
)
.then(personal_ID_arr => console.log(personal_ID_arr))
//will need to operate on 'personal_ID_arr' here after the above finishes or possibly still within the promise1's .then; as long as the arr is filled fully
}
I want to get the results of service calls nested in a forEach loop before executing the next block of code
I've tried using a plain for loop, that works... but ESLINT moans about it.. so what's the ideal solution?
const people = []
const fetchPeople = async () => {
peopleIds.forEach(personId => {
people.push(restApi.getPersonById(personId))
})
}
await fetchPeople()
logger.error(`people should be populated : ${JSON.stringify(people)}`)
I expect people to be populated by the end of the forEach so I can use the results in the next function.
Actually, people is not populated
Easiest way is with Promise.all() and array.map()
const people = await Promise.all(peopleIds.map(restApi.getPersonById));
logger.error(`people should be populated : ${JSON.stringify(people)}`)
Array.map() will create a promise for each of the ids, Promise.all will wait for all of them and output an array of their async returned values.
This question already has answers here:
Using async/await with a forEach loop
(33 answers)
Closed 4 years ago.
I have a forEach loop inside an async function, and inside that forEach are two additional forEach loops that perform AJAX requests. I want to return a resolved promise once all of the AJAX requests are complete.
First, the user clicks "Submit" and I call a function to handle submission, setting the submitting property to true. Once the submission is complete, I want to change the value of the submitting property.
submit() {
this.submitting = true;
if (creating) {
this.create().then(() => this.submitting = false)
}
}
Here's an approximation of the create() method:
async create() {
// Calls a function that uses axios to send a POST request to create
// a new group, returning its ID upon completion. This works fine.
const groupId = await this.createGroup();
// 'users' is an array of objects
this.users.forEach(async user => {
// Create a new user and get their ID. This works fine.
const userId = await this.createUser(info);
// Each 'user' object contains a key called 'attributes' whose
// value is an array of 'attribute' objects. I use axios to make a
// POST request to store the info about these attributes in the DB.
user.attributes.forEach(attribute => {
axios.post('/attributes', attribute)
// In my application, I'm actually calling a method that handles
// making the request, so it looks more like this:
// this.createAttributes(attribute)
}
// Each 'user' object also contains a key called 'images' whose
// value is an array of 'image' objects. Here again, I use axios to
// make a POST request to store the info about these images.
user.images.forEach(image => {
axios.post('/images', image)
}
}
// Once everything has successfully posted, I want to return a
// resolved promise so that the then() callback on my create method
// is executed. However, anything here gets executed before the AJAX
// requests complete.
console.log('All done');
}
I've tried a few different solutions I've come across for capturing promises using map() and using Promise.all to execute a callback once they're all complete, but I haven't had success with that. I think the issue stems from my having multiple nested forEach() loops, but I don't understand promises (and their behavior within forEach() loops specifically) well enough to know for sure.
It's also my understanding that I can use await inside a for...of loop, but I would prefer to execute the AJAX requests in parallel if possible.
Thanks!
Edit: The proposed duplicate is similar, but my question was specifically about async operations within nested forEach loops. While the solution revealed that there is nothing particularly unique about handling these cases (do the same thing on both the parent and nested loop), determining whether this was the case was the motivation for asking the question.
map to promises, then use Promise.all and await them:
await Promise.all(user.images.map(image =>
axios.post('/images', image)
));
Same applies to the main forEach:
await Promise.all(this.users.map(async user => {
//...
}));
I'm trying to make multiple API calls concurrently using the Promise.all method with Axios, based on this example:
getUsers() {
return axios.get('/users');
}
getSessions() {
return axios.get('/sessions');
}
Promise.all([getUsers(), getSessions()])
.then(results => {
// Use the data
})
.catch(error => {
// Catch the error
});
However, as I will only know what concurrent API calls I need to make at this stage based on the result of a previous API call in the promise chain, I'm attempting to pass in an array of anonymous functions to the function in the form of:
var array = [];
array.push(() => {return axios.get('/users')});
array.push(() => {return axios.get('/sessions')});
Promise.all(array).then....
This doesn't work, and I understand that this is because I'm passing in function objects instead of referencing actual Promise objects as the method expects. However, pushing just axios.get(...) methods to the array results in them being called immediately, instead of later when the Promise.all method is executed.
I'm not sure how to do this properly, or whether there's a better approach to achieve what I'm after...
I am not familiar with Axios, but if I understand correctly axios.get returns a Promise, which you need for Promise.all. How about:
Promise.all(array.map(f => f()).then....
That way your functions are called when you actually want, and the map will give you an array of their results, so an array of Promises.
Note that this is essentially the same as the example you have cited with [getUsers(), getSessions()] – with the difference that your functions are anonymous and called implicitly using the map, instead of explicitly by their names. This way you have more flexibility in which functions actually get called.
Basically, in siplest scenario, to use anonymous function as a wrapper, you need to call it before you are put it to Promise.all.
So just try to :
const f = async () => {return service.myFunc();}
Promise.all([f()])
so, your wrapper (f) calling before, and you are providing array with one Promise.
or
Promise.all([(async () => {const result = await service.myFunc(); return result;})()]);
or (equal) :
const myPromise = (async () => {const result = await service.myFunc(); return result;})();
Promise.all([myPromise]);
So Promise will be provided to Promise.all, not just raw function. Also, be carefully and do not forget to use await in case of "big anonym function body".
i've been scratching my head about this for the longest time. in the first example you call the functions here:
Promise.all([getUsers(), getSessions()])
but in the second the anonymous functions are just stated, never called:
array.push( ()=>{return axios.get('/users')} );
so you would need to do this and call them as you pushed them:
array.push( (()=>{return axios.get('/users')}) () );