pushing api responses to global variable inside .map() somehow makes data inaccessible - javascript

I created a function that makes a series of API calls through a Promise like this
//userApiList is a variable with an array of links
return Promise.all(userApiList.map(url =>{
var a = $.getJSON(url);
//userData is a global variable
userData.push(a);
return a;
}));
I want to both save the data to a variable for later use and return it right away to be iterated into html with jquery. Everything loads up perfectly as expected, however when I go to access the data in the variable I get all "undefined" properties. I console.logged the variable and it shows the data is there, I click the buttons well after the data is fetched so it's not an async issue. here's an example of one of the functions
$("#load_online").click(function(){
var users = [];
for(var i = 0; i < userData.length; i++){
var status = userData[i].status;
if(status !== null || status !== undefined){ users.push(userData[i]); }
}
$("#result_frame").empty();
//loadUsers() iterates the data into html
loadUsers(users);
});
I tried console.log(userData[i].status) just to see what the results would be and I got 200 as a response when it's suppose to be null' or the title of an episode being streamed on the channel.
The problem is I thought the responseJSON: field is what's always returned for use, as I've never walked into this issue before. This time it seems the whole object is being read therefore userData[i].status was reading the status: property a layer up rather than inside the responseJSON: object. I tried thinkering with getting a response out of userData[i].responseJSON.status and that returned undefined for each object. Does anybody readily see what I'm doing wrong? here's a CodePen of the overall project if you need a closer look at things.

You're not pushing the data into your array, you're pushing a jQuery jqXHR object (which is thenable, e.g., Promise-like) into it (that's what the return value of $.getJSON is). The reason the Promise.all result works is that Promise.all waits for those thenables to resolve.
It's quite likely you don't want to have a userData global at all. Instead, have whatever code needs the data get it from a resolution handler on the the promise returned by Promise.all.
But if you really want to populate userData, wait for resolution:
return Promise.all(userApiList.map($.getJSON))
.then(results => {
userData.push(...results);
return results;
});
Note that userData won't have the data until the ajax requests complete (of course).
The above won't add the results to userData until they're all avaialble. You also have the option of populating it as you go:
return Promise.all(userApiList.map(url => $.getJSON(url).then(result => {
userData.push(result);
return result;
})));
But it's important to remember the results will be added over time, not instantaneously, as the requests complete.
If you wait for all of them, Promise.all will ensure they're in the same order as the promises you giave it; if you add them as you go, they may not be in that order (because you're pushing as each one completes, and an earlier one may complete after a later one).

agreed with the upper answer,
this may work too,
return userApiList.map(url => { $.getJSON(url).done(function(results){
userData.push(results)
});})

Related

What's the point of returning something from an Async/Await Function?

I've created a code snippet here that fetches data about a certain country using REST Countries API. The function works fine, and returns a pending promise. That promise's fulfilled value will equal an object containing key value pairs for the country's capital, name, and code.
But if I wanted to use them for something, why wouldn't I just set that very same object equal to a variable and continue to program new actions inside my async function? Why bother trying to use values gained asynchronously on the global scope?
function getJSON(url, errorMSG = 'Something went wrong') {
// feed it the fetch URL, then your customized error message
return fetch(url).then(response => {
if (!response.ok) throw new Error(errorMSG);
return response.json();
});
}
async function countryData(nation) {
try {
const info = await getJSON(
`https://restcountries.eu/rest/v2/name/${nation}?fullText=true`,
'Invalid country selected'
);
return {
// Fullfilled value of promise
capital: info[0].capital,
name: info[0].name,
code: info[0].cioc,
};
} catch (err) {
console.error(err); // display your custom error message
}
}
console.log(countryData('Canada'));
fetch is an async function. Why do they resolve the promise to a response object, instead of continuing to run extra actions once they have it? Because they don't know what you want to do with it. It would be impossible for fetch to handle every possible thing that should happen next, so it just has a single job: get the data, then return it (in a promise). You can then combine this with whatever other code you like.
On a smaller scale, this may happen with countryData too. You might have 10 different parts of your app that want to do things with the result from countryData. It may not be "impossible" for countryData to implement all 10 things, but it's definitely impractical and not a good idea. Instead, countryData can be written to have one job: get the country data and return it. Then each of the 10 pieces of code can do their own things with the result.
This isn't about it being async, the same principles apply to synchronous code to. If you can keep code focused on a single task, without entangling it with the needs of other pieces of code, then your code becomes easier to maintain.

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 nested fetch API

I started working on an android app. I am at the point that i need to use an API to get some info. I am using an API for movies so it contains id, title and some other info. My problem is that in this URL i don't hav every information that i need so i have to make another fetch for every movie to get the additional info but as i noticed first JS runs the code in the first fetch ( without the nested fetch ) and then it runs the nested fetch. This way some info are not displayed on the screen.
fetch(movie)
.then(function(response) { return response.json(); })
.then(function(responseObject) {
movies.value = responseObject;
for(var i=0;i<movies.value.results.length;i++)
{
movies.value.results[i].poster_path = poster+movies.value.results[i].poster_path;
var info = "http://api.themoviedb.org/3/movie/"+movies.value.results[i].id+"?api_key";
fetch(info)
.then(function(response) { return response.json(); })
.then(function(responseObject) {
moreInfo.value = responseObject;
if(j < movies.value.results.length)
{
movies.value.results[j].runtime = moreInfo.value.runtime;
console.log("print 1 "+movies.value.results[j]);
j++;
}
});
console.log("print 2 "+movies.value.results);
}
});
what i am trying to do here is add from the second fetch the runtime to my movies Observable object. A sortened version of the result :
print 2 : movies.value.results
print 1 : movies.value.results[i]
The problem as i said is that the code inside the first fetch is executed without executing the nested fetch and then the nested fetch executes. Sorry if i did something really bad but i just started developing on android so please show me mercy :D
*Sorry for not explaining about my j variable. It is defined as zero above fetch(movie). The only reason i use it is because i inside fetch(info) is always at the max number i cang get from movie.value.results.length and because i need to pass the runtime at the specific movie.value.result[j]. So i have a counter that increases only when the fetch(info) is executed.
This snippet might have some other problems too, like pointed out in the comments, but I think I know where the main confusion comes here.
The problem as i said is that the code inside the first fetch is executed without executing the nested fetch and then the nested fetch executes.
This is not true. Or, at least the order of console logs doesn't imply that. The order of execution inside the outer then-callback is
fetch(info) This statement starts fetching the data
.then(function(response){ ... }) Register a callback to execute when the promise gets resolved. Note that as fetching is asynchronous, the callback function does not get called right away. It will get called later, when the data has finished loading. Meanwhile, synchronous execution of the call stack continues.
.then(function(responseObject) { ... }) Same thing here. We create a function to run when the data is ready (if ever). Meanwhile the statements outside that function can still be evaluated.
console.log("print 2 "+movies.value.results); Now here comes the first log statement. The JavaScript execution does not halt to wait for the fetch to finish. That's the whole idea of asynchronous programming.
Now, some time passes and finally, the data has finished loading.
return response.json(); The Promise returned from the fetch gets fulfilled because the operation was successful. It then runs the callback that you provided earlier.
moreInfo.value = responseObject;
if(j < movies.value.results.length)
{
movies.value.results[j].runtime = moreInfo.value.runtime;
console.log("print 1 "+movies.value.results[j]);
j++;
}
This is the second callback we created and same thing applies here. Now that the data is finally ready, this can be executed. I'm not sure about the whole j thing, but apparently the execution will eventually get to the console.log("print 1 "+movies.value.results[j]); statement.
The important thing to understand here is that whether or not the 'print 1' statement was in the source code above the 'print 2' line is irrelevant. The 'print 1' was scheduled for later, namely, when the second fetch is done.
Fixed version
var info = //URL
fetch(info)
.then(function(response) { return response.json(); })
.then(function(responseObject) {
moreInfo.value = responseObject;
...
console.log("print 1 "+movies.value.results[j]);
...
})
.then(function(){
console.log("print 2 "+movies.value.results);
});
Now it could also be that I totally misunderstood the question. But if this was about asynchronicity, you should also check out the MDN Promise docs linked above and this SO question about asynchronicity in Node.js, which also applies to other JavaScript environments.

How to extract data out of a Promise

I have a promise that returns data and I want to save that in variables. Is this impossible in JavaScript because of the async nature and do I need to use onResolve as a callback?
Can I somehow use this (e.g. wrap it with async/await):
const { foo, bar } = Promise.then(result => result.data, errorHandler);
// rest of script
instead of this?
Promise.then(result => {
const { foo, bar } = result.data;
// rest of script
}, errorHandler);
Note: Bluebird library is used instead of native implementation, and I can't change from Promise to asnyc/await or Generators.
NO you can't get the data synchronously out of a promise like you suggest in your example. The data must be used within a callback function. Alternatively in functional programming style the promise data could be map()ed over.
If your are OK using async/await (you should it's awesome) then you can write code that looks synchronous yet retain the asynchronicity of a promise (see #loganfsmyth comments).
const { foo, bar } = await iAmAPromise.then(result => result.data);
Overall since you are already using ES6 I assume you are also using a transpiler. In which case you should definitely give async/await a try.
Just be sure to weight in the decision that as today they are not yet a ratified specification.
While you can get a value from an awaited Promise inside an async function (simply because it pauses the function to await a result), you can't ever get a value directly "out" of a Promise and back into the same scope as the code that created the Promise itself.
That's because "out of" would mean trying to take something that exists in the future (the eventually resolved value) and putting it into a context (synchronous variable assignment) that already happened in the past.
That is, time-travel. And even if time-travel were possible, it probably wouldn't be a good coding practice because time travel can be very confusing.:)
In general, if you find yourself feeling like you need to do this, it's good sign that you need to refactor something. Note that what you're doing with "result => result.data" here:
Promise.then(result => result.data, errorHandler);
// rest of script
..is already a case of you working with (literally, mapping over) the value by passing it to a function. But, assuming that "// rest of script" does something important related to this value, you probably want to continue mapping over the now updated value with yet another function that then does something side-effect-y with the value (like display the data on the screen).
Promise
.then(({ data }) => data)
.then(data => doSomethingWithData(data))// rest of script
.catch(errorHandler);
"doSomethingWithData" will be called (if it's ever called) at some unknown point in the future. Which is why it's a good practice to clearly encapsulate all that behavior into a specific function and then hook that function up to the Promise chain.
It's honestly better this way, because it requires you to clearly declare a particular sequence of events that will happen, explicitly separated out from the first run through all of your application code's execution.
To put it another way, imagine this scenario, hypothetically executed in the global, top-level scope:
const { foo, bar } = Promise.then(result => result.data, errorHandler);
console.log(foo);
//...more program
What would you expect to happen there? There are two possibilities, and both of them are bad.
Your entire program would have to halt and wait for the Promise to execute
before it could know what "foo" & "bar" would... nay, might be. (this is
what "await," inside an async function, does in fact do: it pauses
the entire function execution until the value is available or an the error is thrown)
foo and bar would just be undefined (this is what actually
happens), since, as executed synchronously, they'd just be
non-existent properties of the top-level Promise object (which is not itself a "value,"
but rather a quasi-Monadic wrapper around getting an eventual value OR an error) which most
likely doesn't even contain a value yet.
let out; mypromise.then(x => out = x); console.log(out)
Only use this code when
you are debugging by hand,
and you know the promise has already succeeded
Behaviour of this code:
While the promise has not resolved yet, out will be undefined.
Once the promise resolves to a failure, an error is thrown.
When the promise resolves to success, (which may be after the console.log), the value of out will change from undefined to the Promise result — maybe in the middle of what you were doing.
In production code, or really any code that runs without you, the code that uses the Promise result should be inside the .then() callback, and not use some out variable. That way:
your code won't be run too early (when the result is still undefined),
and won't run too late (because you don't need 'I think sleeping for 10 seconds should do it' workarounds),
and won't erroneously run when the promise fails.
I have a solution of getting this value "out" if you will. This is a method at backend for uploading multiple files to AWS S3 which must be dealt asynchronously. I also need the responses from S3, so I need the values out of the Promise:
async function uploadMultipleFiles(files) {
const promises = []; //Creating an array to store promises
for (i = 0; i < files.length; i++) {
const fileStream = fs.createReadStream(files[i].path)
const uploadParams = {
Bucket: bucketName,
Body: fileStream,
Key: files[i].filename
}
promises.push(s3.upload(uploadParams).promise()) //pushing each promise instead
//of awaiting, to enable for concurrent uploads.
}
await Promise.all(promises).then(values => {
console.log("values: ", values) //just checking values
result = values; //storing in a different variable
});
return result; //returning that variable
}
The key lines in context with the issue being discussed here are these :
await Promise.all(promises).then(values => {
console.log("values: ", values) //just checking values
res = values; //storing in a different variable
});
return res; //returning that variable
But of course we have to also await in the function that will be calling this :
const result = await uploadMultipleFiles(files);
All you need to do is to extract all you have in your promise by using a .then
yourFunction().then( resp => {
... do what you require here
let var1 = resp.var1;
let var2 = resp.var2;
...
.....
})
yourFunction() should return a Promise
How to Get A Value From A Promise
YES! You can extract value out of a promise!
Do NOT let anyone here say you cannot. Just realize any variable that stores your returned promise value will likely have a short delay. So if you have a JavaScript script page that needs that data outside of the Promise or async-await functions, you may have to create loops, interval timers, or event listeners to wait to grab the value after some time. Because most async-await-promises are REST calls and very fast, that wait would require just a quick while loop!
It is easy! Just set a variable (or create a function) that can access the value inside your async or promise code and store the value in an outside variable, object, array, etc you can check on. Here is a primitive example:
// I just created a simple global variable to store my promise message.
var myDelayedData = '';
// This function is only used to go get data.
// Note I set the delay for 5 seconds below so you can test the delay
const getData = () => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve('my promise data'), 5000);
});
}
// I like to create a second async function to get the data
// from the promise object and save the data to my global variable.
const processData = async () => {
let data = await getData();
// Save the delayed data to my global variable
myDelayedData = data;
}
// Start the data call from the promise.
processData();
// Open up your browser, hit F12 to pull up the browser devtools
// Click the "console" tab and watch the script print out
// the value of the variable with empty message until after
// 5 seconds the variable is assigned to the resolved promise
// and apears in the message!
// THAT IS IT! Your variable is assigned the promise value
// after the delay I set above!
// TEST: But let's test it and see...
var end = setInterval(function(){
console.log("My Result: " + myDelayedData);
if(myDelayedData !== ''){
clearInterval(end);
}
}, 1000);
// You should see this in devtools console.
// Each line below represents a 1 second delay.
My Result:
My Result:
My Result:
My Result: my promise data
Most people seeing this code will say "Then why use a Promise, just make a call for the data, pause, and update your application?" True: The whole point of a Promise is to encapsulate data processes inside the promise and take actions while the rest of the script continues.
But... you may need to output a result outside the Promise. You may have other global processes that need that data because it changes the state of the global application, for example. But at least you know you can get to that Promise data if you needed it.

Categories

Resources