Struggling to get my promises to work in an array loop - javascript

I have been struggling with this issue for a week and have researched myself close to death. I am a total newbie. I have managed to grasp the crux of promises, but I am failing to see how to include this in a loop.
I have an app that is looking through an array. There is some validation of the array against a mongoose database (which is taking time to run). I am trying to push items into a new array based on some of this validation. I know the validation is working because of the console log in the loop. However my final array is not waiting for the loop to finish. Which means I need to put the loop into a promise, or well I think, but the issue is that I don't know how to resolve it. Current output is a blank array instead of the validated array. Here is my code:
//dummy data of an array - this is originally extracted from a mongoose DB and works (it's my first promise).
const appArray = ["5f8f25d554f1e43f3089ea5d",
"5f8f25e854f1e43f3089ea5e",
"5f8f25f454f1e43f3089ea5f",
"5f8f314ab92c7f406f28b83a",
"5f8fe50a9d44694cad91a01b",
"5f92e8a75d848870e015dff3",
"5f92e8b35d848870e015dff4",
"5f92e8cb5d848870e015dff5",
"5f8fe51d9d44694cad91a01c"];
//the second promise takes the array above and validates it against another collection on mongoose
function myPromise2 (response){
return new Promise((resolve, reject) => {
let appoints = [];
response.forEach(e => {
//loop through each item of the array and look against Appointment collection
Appointment.findById(e, function(err, foundApp){
//some validation supposed to happen here and then pushed into a new array
appoints.push(foundApp);
console.log(appoints);
})
})
//once completed supposed to resolve and return
resolve(appoints);
})
};
myPromise2(appArray).then((response) => {console.log(response)});

Here is an example which should work. Add a promise for each element to the array and then resolve the outer function if all promises have resolved.
// dummy data of an array - this is originally extracted from a mongoose DB and works (it's my first promise).
const appArray = ["5f8f25d554f1e43f3089ea5d",
"5f8f25e854f1e43f3089ea5e",
"5f8f25f454f1e43f3089ea5f",
"5f8f314ab92c7f406f28b83a",
"5f8fe50a9d44694cad91a01b",
"5f92e8a75d848870e015dff3",
"5f92e8b35d848870e015dff4",
"5f92e8cb5d848870e015dff5",
"5f8fe51d9d44694cad91a01c"];
// the second promise takes the array above and validates it against another collection on mongoose
function myPromise2 (response) {
let appoints = [];
response.forEach(e => {
appoints.push(new Promise((resolve) => {
//loop through each item of the array and look against Appointment collection
Appointment.findById(e, function(err, foundApp) {
//some validation supposed to happen here and then pushed into a new array
resolve(foundApp);
})
}))
})
return Promise.all(appoints)
};
myPromise2(appArray).then((response) => {console.log(response)});

Points to address:
Use the promise that mongoose provides through the .exec() method. This way you don't need new Promise
Collect these individual promises in an array (use .map instead of .forEach), and pass this array to Promise.all
If you do this, the code for myPromise2 reduces to the following:
function myPromise2 (response){
return Promise.all(response.map(e => Appointment.findById(e).exec()));
}

here is my suggestion:
const appArray = [
"5f8f25d554f1e43f3089ea5d",
"5f8f25e854f1e43f3089ea5e",
"5f8f25f454f1e43f3089ea5f",
"5f8f314ab92c7f406f28b83a",
"5f8fe50a9d44694cad91a01b",
"5f92e8a75d848870e015dff3",
"5f92e8b35d848870e015dff4",
"5f92e8cb5d848870e015dff5",
"5f8fe51d9d44694cad91a01c"
];
function myPromise2 (response){
return Promise.all(response.map(id) => {
return Appointment.findById(id).exec();
})
};
myPromise2(appArray)
.then(console.log) // response array
.catch(// handle errors)
// you can also async/await the calling part
you can also use one of:
Promise.allSettled
Prmoise.any (es2021)
Promise.race
it's just depends on how you would like to handle the responses/failures.

A good alternative to consider maybe Async/Await and have a look Async_await. This will hopefully answer all your issues
It's probably a good idea to look into how the JS event loop system works guide here,

Related

Resolve a promise nested inside an array of objects

I have an array of objects that I get from database.
const users = await User.findAll();
To each of them, I want to call an async function user.getDependency() to get another related table, and devolve it nested in the object to a final format like this:
[
User {
attr1: value,
attr2: value,
...
Dependency: {
attr1: value,
...
}
},
User {
...
and so on
Now, I get my problem. The only possibilities I am being able to think involving async and loops are 2:
Option 1: mapping them and getting the promise back:
users.forEach((user) => {
user.dependency= user.getDependency();
});
With this I get similar to the desired outcome in terms of format, only that instead of the resolved thing i get obviously Dependency: Promise { <pending> }, that is, the promises all nested inside each user object of the array. And I don't know how to procceed with it. How to loop through all the user objects and resolve this promises they have inside?
Option 2: return the promises to be resolved in Promise.all
const dependencies = Promise.all(users.map((user) => {
return user.getDependency();
}));
This give me a separated array with all the dependecies I want, but that's it, separated. I get one array with the objects users and another with the objects dependencies.
What is bugging me specially is that I am thinking that there must be a simple straightforward way to do it and I am missing. Anyone have an idea? Much thanks
To build some intuition, here is your forEach example converted with a push in the right direction.
users.forEach((user) => {
user.getDependency().then(dependency => {
user.dependency = dependency
})
})
The above reads "for each user, start off the dependency fetching, then add the dependency to the user once resolved." I don't imagine the above is very useful, however, as you would probably like to be able to do something with the users once all the dependencies have finished fetching. In such a case like that, you could use Promise.all along side .map
const usersWithResolvedDependencies = await Promise.all(
users.map(user => user.getDependency().then(dependency => {
user.dependency = dependency
return user
}))
)
// do stuff with users
I should mention I wrote a library that simplifies async, and could certainly work to simplify this issue as well. The following is equivalent to the above. map and assign are from my library, rubico
const usersWithResolvedDependencies = map(assign({
dependency: user => user.getDependency()
}))(users)
I guess the best way is to do like this. Mapping users, updating every object (better to do this without mutation), and using Promise.all
const pupulatedUsers = await Promise.all(users.map(async (user) => ({
...user,
dependency: await getDependency(user.dependency_id)
// or something like getDependency
// function that fires async operation to get needed data
}));
you're right there is a straightforward way to resolve the promise
as the users variable is not a promise waiting to be resolved, you should try
users.then((userList) => {
// function logic here
})
the userList in the then function should be iterable and contain the desired user list
also you can also handle the rejected part of the promise, if at all the query fails and the promise returns an error:
users.then((error) => {
// error handling
})
you should give this document a glance to get more info about handling promises to further know what goes on in that query when it returns a promise

JS - Failure to correctly settle an array of promises before moving on to the next part

I've been encountering an issue regarding JS promise use, and hopefully it is simply that I am missing something very obvious.
Essentially, I attempt to read multiple JSON files at once and push their content to an array belonging to another object, then perform operations on the elements on this array. Therefore, the array needs to be filled before operations are attempted on them. However, despite me using promises to theoretically make sure the order is correct, it seems what I've written fails at doing that.
How do I fix this issue?
Here are snippets of the code I'm using, where the issue arises:
This is the function where I push the extracted objects to my array:
function pushNewRoom (ship, name_json_folder, elem, id) {
let promiseRoom = new Promise ((resolve, reject) => {
let newRoom = gf.getJSONFile(name_json_folder + '/' + elem + ".json")
// note: getJSONFile allows me to grab a JSON object from a file
.then(
(data) => {
data.name = elem;
ship.rooms.push(data);
return data;
}).then((newRoom) => {
resolve(newRoom);
}).catch((reject) => { // if the JSON file doesn't exist a default object is generated
let newRoom = new Room (elem, id);
ship.rooms.push(newRoom);
resolve(newRoom);
});
});
return promiseRoom;
}
And this is the part that calls that function and performs the operations I need after that:
exports.generateRoomsByLayout = function (name_json_folder, ship)
{
ship.rooms = [];
console.log("reached step 1");
// First execution step: get a JSON file
gf.getJSONFile(name_json_folder + "/_base_layout.json")
.then(function (shipmode){
// Note: shipmode is a JSON object that acts as a blueprint for the operations to follow.
// Importantly here, it contains an array, layout, containing the names of every other JSON file I will need to perform the operations.
console.log("reached step 2");
Promise.allSettled(shipmode.layout.map(function (elem, index){pushNewRoom(ship, name_json_folder, elem, index);})
// note: this is where my issue happens
).then(function (){
console.log("reached step 3");
// Operations on the results that were pushed to ship.rooms by pushNewRoom()
}).then(function (shipmode) {
console.log("reached step 4");
// More operations on the results
}).catch((err) => {
});
return Promise.resolve(shipmode);
}).catch(function (errRejection) {
// Error handling
console.log(errRejection);
});
};
The issue happens right at the Promise.allSettled() line. Rather than waiting for the promises supposedly generated with ship.layout.map(), that would then become an iterable array, the program continues on.
I suppose this is because Promise.allSettled() does not wait for the array to be generated by map() before moving on, but have been unable to fix the issue, and still doubt that this is the explaination. Could anyone enlighten me on what I am doing wrong here?
If what I'm asking is unclear then please do tell me, and I'll try my best to clarify.
edit: I suspect it is linked to Promise.allSettled() not waiting until map() fills the array to consider every promise inside the array settled, as its length seems to be 0 right at step 3, but I am not sure.
Nevermind, I'm an idiot.
The map() method's callback (?) won't (obviously) return an object (and therefore, a promise) if you do not tell it to. Therefore,
shipmode.layout.map(function (elem, index){pushNewRoom(ship, name_json_folder, elem, index);})
needs to be
shipmode.layout.map(function (elem, index){return pushNewRoom(ship, name_json_folder, elem, index);})

Promise.all function resolving before promise completion with adding data to arrays

I'm trying to export an array of data objects for later use, though I can use promises to wait till all data has been added to the array before logging, I noticed when I went to use that data I couldn't because even with Promise.all, the length of the array was still zero as if nothing had changed.
I tried having the console log each time the doc.data() was pushed to the exportArray and I noticed that it logs that after it outputs the array. So for example...
Expected Output
doc.data() // For Each doc
Array[] // Filled with data and length 54
Length: 54
Actual Output
Array[] // Filled with data and length 54
Length: 0
doc.data() // For Each doc
let exportArray = [];
let promises = [];
db.collection('lists').doc('List 1').collection("members")
.get().then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
promises.push(
new Promise(function (resolve, reject) {
exportArray.push(doc.data());
resolve();
console.log('before');
})
);
});
});
Promise.all(promises).then(function () {
console.log(exportArray); // Logs correctly with all data with length 54
console.log(exportArray.length); // Logs as 0 for some reason
});
Ideally this should output the exportArray with it's data AND the length being 54. However it does output the data but the length is output as 0. (and yes I clicked on the data array in console and it shows a length of 54)
Why does the array get populated but I'm unable to use methods on it like exportArray.length correctly?
You must call Promise.all on promises after it has been filled with promises, which happens asynchronously in a then callback. Now you execute it synchronously when none of that has happened yet.
So do:
db.collection('lists').doc('List 1').collection("members").get().then(function(querySnapshot) {
let promises = querySnapshot.docs.map(function(doc) { // <-- use docs.map
return doc.data(); // <-- just return `data()`. No need for a new promise
});
// Must be here:
return Promise.all(promises).then(function (exportArray) { // <--- data arg!
console.log(exportArray);
console.log(exportArray.length);
});
});
Notes:
There is no need for new Promise when you have the value to resolve with readily available.
Instead of forEach, get the array from the query snapshot with .docs and the JS built-in .map().
The fact that you see the array in the console but with a length 0 is the behaviour of the console: it only logs the reference to the array, but then when you expand it in the console, it has in the mean time been populated; so you see the data. But it was not there at the moment of the logging, which is what the length: 0 is telling you.
Simplification
According to the firebase documentation, doc.data() returns the data, not a promise, so there is no reason to use Promise.all, a simple map should suffice:
db.collection('lists').doc('List 1').collection("members").get().then(function(querySnapshot) {
return querySnapshot.docs.map(function(doc) {
return doc.data();
});
}).then(function (exportArray) {
console.log(exportArray);
console.log(exportArray.length);
});
Since db.collection('lists').doc('List 1').collection("members").get() returns immediately with a promise that resolves only after the query completes, your code will go on to execute Promise.all() against an empty list and also return immediately because there's nothing to wait on. Some time after that, your snapshots will be ready and promises will be populated.
You should call Promises.all() only after the entire array has been populated.
I'm not very familiar with the db you use, but just fixing the antipatterns in your code should solve your problem. I guess that's about what your code should look like.
db.collection('lists')
.doc('List 1')
.collection("members")
.get()
.then(querySnapshot => querySnapshot.map(doc => doc.data())
.then(promises => Promise.all(promises))
.then(exportArray => {
console.log(exportArray); // Logs correctly with all data with length 54
console.log(exportArray.length); // Logs as 0 for some reason
})
About the antipattern:
avoid using new Promise() and similar. it's rarely necessary, ususally you already have a Promise chanin you can derive from.
Promises are wrapping values that you don't have yet. Don't try to "unwrap" these values by doing something along the lines of somePromise.then(value => { externalVariable = value; }) or in your case it's an Array.
You now have a variable that will (at some point in the future) contain the value you want, but at the moment is empty/invalid. So now you have to implement your own state management to check when the value has become valid; basically duplicating most of the logic of the Promise ;)

javascript promise still executing code synchronously

so basically i have a web application that retrieves data from firebase. and since it takes a lot of time to retrieve data from firebase, i used promise to make my code to populate at the right time. here is my code:
var promise = getDataFirebase();
promise.then(function () {
console.log(Collect);
console.log("firsst");
return getDataFirebaseUser();
}).then(function () {
console.log("Second");
});
function getDataFirebase() {
return new Promise(function (resolve, reject) {
refReview.on("value", function (snap) {
var data = snap.val();
for (var key in data) {
Collect.push({
"RevieweeName": data[key].revieweeID.firstname.concat(" ", data[key].revieweeID.lastname),
"ReviewerName": data[key].reviewerID.firstname.concat(" ", data[key].reviewerID.lastname),
rating: data[key].rating,
content: data[key].content,
keyOfReviewee: data[key].revieweeID.userID
})
var getDataToUsers = firebase.database().ref("users").child(data[key].revieweeID.userID);
getDataToUsers.once("value", async function (snap) {
var fnLn = snap.val();
var first = fnLn.isTerminated;
console.log("terminateStatus", first);
});
}//end of for loop
resolve();
}); //end of snap
});
}
so in function getDataFirebase, data is retrieved from firebase and is push to an array called , Collect. So, after pushing 1 row, it query again to the firebase for another table of data then continues the loop. The problem here is that, i wanted to finish all processes before it resolves the promise.
the output of console according to the code is as follow:
Collect (array)
first
Second
terminateStatus, 1
it must be
Collect (array)
first
terminateStatus,1
second
I'm not 100% sure how your code is working but it looks like you're doing the right thing on refReview.on("value"): creating a promise before calling Firebase, and then resolving it afterwards. But you're not doing that on getDataToUsers.once("value"). There, you're firing the event and not waiting for it to return — the for-loop continues on, and all the callbacks are processed later, but resolve is at the end of the for-loop, so it's too late by then.
My guess is that you thought the async keyword would cause the for-loop to wait for that job to complete, but actually all it does here is cause the callback to return a promise — which is ignored by the on function.
You have a few options, but probably the best will be to use Promise.all, which accepts an array of promises and waits for them all to be resolved — but you should probably use Firebase's Promise API rather than bolting promises onto the event API. So it'll look something like:
refReview.once('value')
.then(snap => Promise.all(snap.val().map(item =>
firebase.database()
.ref("users")
.child(item.revieweeID.userID)
.once('value')
.then(snap => {
console.log('terminated');
])
})));
(except with the actual functionality added in, natch)

Async findOne() operation inside forEach Loop

I am having a hard time understanding JavaScript Promises. I am searching on of my Mongoose models for objects that meet a certain condition and if they exist, I want to make the object into a plain JS object and add a property onto it.
Unfortunately, I am unable to wrap my head around how I can ensure my forEach loop will run completely before my promise ends up resolving. Please see my code.
// Called to check whether a user has participated in a given list of challenges
participationSchema.statics.getParticipation = function(user, challenges) {
return new Promise((resolve, reject) => {
challengesArray = [];
challenges.forEach((challenge) => {
// Model#findOne() is Async--how to ensure all these complete before promise is resolved?
Participation.findOne({user, challenge})
.then((res) => {
if (res) {
var leanObj = challenge.toObject();
leanObj.participation = true;
challengesArray.push(leanObj);
}
})
.catch(e => reject(e));
})
console.log("CHALLENGES ARRAY", challengesArray); // Challenges Array empty :(
resolve(challengesArray);
});
}
I've looked through similar questions, but am unable to get to an answer. Appreciate the help.
So, what is happening when you call getParticipation is that the forEach loop runs all the way and all individual promises for Participation.findOne are created but not yet resolved. The execution doesn't wait for them to resolve and continues after the forEach, resolving the top-level promise challengesArray, which is still empty at that point. Sometime after, the promises created in the forEach start resolving but their results are now lost.
Also, as Bergi mentioned in the comments, nesting promises is considered an anti-pattern; promises should be chained, not nested.
What you want is to use something like Promise.all to wait for all of your promises to finish first, then you filter out all non-existing results and finally return the array.
participationSchema.statics.getParticipation = function(user, challenges) {
return Promise.all(challenges.map(challenge => {
return Participation.findOne({user, challenge}).then(result => {
if (result) {
var leanObj = challenge.toObject();
leanObj.participation = true;
return leanObj;
}
});
})
// at this point, results contains an array of `leanObject` and `undefined` depending if the `findOne` call returned anything and the code withing the `if` above was run
.then((results) => {
return results.filter(result => !!result) // filter out `undefined` results so we only end up with lean objects
});
}

Categories

Resources