I'm trying to read some data from Firestore using an array of keys and a map function, then, when all the data is back from Firestore, run a function on the resulting data set. This code works, but not in the right order; I'm struggling to make doSomethingUsefulWithData() delay until arrayOfDataFromFirestore is complete.
I've tried await but I get an error message saying "Uncaught syntax error: await is only valid in async functions and async generators". I think the awaits shown below are in the async function but apparently not.
If I run the version shown here, doSomethingUsefulWithData() runs before getData(), despite following it with a .then.
var arrayOfFirestoreKeys = [key0, key1, key3];
function doSomethingUsefulWithData(){
//Do something useful with the data here
};
function dataGet(){
var arrayOfDataFromFirestore = [];
arrayOfDataFromFirestore = Promise.all(arrayOfFirestoreKeys.map(async (dataGet, index) => {
var docRef = db.collection('dataInFirestore').doc(arrayOfFirestoreKeys[index]).get().then((doc) => {
if (doc.exists) {
console.log("Document data from async:", doc.data());
//doSomethingUsefulWithData here works but runs n times for n items in arrayOfFirestoreKeys
};
//Trying await doSomethingUsefulWithData here produces warning 'await must be in async function or function generator'
});
//Trying await doSomethingUsefulWithData here produces warning 'await must be in async function or function generator'
}))
return(arrayOfDataFromFirestore);
};
arrayOfDataFromFirestore = dataGet().then(doSomethingUsefulWithData(arrayOfDataFromFirestore));
//With this version of the code,
//doSomethingUsefulWithData runs first and produces a complaint that arrayOfDataFromFirestore is undefined.
//Then the contents of arrayOfDataFromFirestore follow into console.log
If I correctly understand your question, the following should do the trick:
var arrayOfFirestoreKeys = [key0, key1, key3];
function doSomethingUsefulWithData(arrayOfSnapshots) {
// arrayOfSnapshots is an Array of DocumentSnapshot
arrayOfSnapshots.forEach(docSnap => {
if (docSnap.exists) {
// Do something
}
});
}
function dataGet() {
const arrayOfDataFromFirestore = arrayOfFirestoreKeys.map((k) =>
db.collection('dataInFirestore').doc(k).get()
);
return Promise.all(arrayOfDataFromFirestore);
}
dataGet().then(array => {
doSomethingUsefulWithData(Array);
});
Related
I am getting url from firebase using ".getDownloadUrl" and then push into an array but array shown empty but inside data available in console
I am using the following code:
const getPostUrl = async (item) => {
const url = await storage().ref(item.data?.postName).getDownloadUrl();
return { ...item._data, URL: url };
};
const getUrls = async () => {
let response=[]
storage().list((data)=>console.log(data, "knok knok tera baaap aaya"))
PostCollection.doc(UserInfo.uid)
.collection("post")
.onSnapshot(documentSnapshot => {
documentSnapshot.docs.forEach ( async (item, i) => {
const getdata = await getPostUrl(item)
response.push(getdata)
// const url = await storage().ref(item._data?.postName).getDownloadUrl()
// const obj = { ...item._data, URL:res }
});
// setPosts(postArr);
// setUrls(newArr);
});
let xyz = await Promise.resolve(response)
console.log(response, xyz, "RESPONSE========>");
};
Note: Originally code provided as image: here
Diagnosis
Your code is executed asyncronously. In simple terms that means that conceptually there is no guarantee of execution order among code portions flagged as async and wrt to the (synchronous) rest of the code.
Specifically that applies to the handler passed as argument to .forEach where the array is filled and the console output statement.
What likely happens is that the array is written to the console before the code to fill it with downloaded information has executed.
Remedy
Add additional synchronization to guarantee that the array is filled before using/outputting it.
Implementation
Instead of await-ing the result of async execution in various places of the code, pass the abstraction of the future results as Promises. In particular, collect the responses as an array of Promises instead of an array of results. If all promises are resolved (in production code you would prefer to have them all 'settled', as some may fail), continue processing, eg. output to console.
Pitfalls
Promise.all (For the in-browser implementation, see here (MDN), available in most Promise libraries) processes the argument array as is without regard to whether it contains the proper number of arguments. This is no problem here as the array-filling forEach loop is executed synchronously but must be looked after when modifying the code.
Beware: The following code is untested !
const getPostUrl = (item) => {
const p_url =
storage().ref(item.data?.postName).getDownloadUrl()
.then ( (url) => { return Promise.resolve({ ...item._data, URL: url }, (e) => { /* flag error */ } )
;
return p_url;
};
const getUrls = async () => {
let p_response=[]
storage().list((data)=>console.log(data, "knok knok tera baaap aaya"))
PostCollection.doc(UserInfo.uid)
.collection("post")
.onSnapshot(documentSnapshot => {
documentSnapshot.docs.forEach ( (item, i) => {
const getdata = getPostUrl(item)
p_response.push(getdata)
// const url = await storage().ref(item._data?.postName).getDownloadUrl()
// const obj = { ...item._data, URL:res }
});
Promise.all ( p_response )
.then ( (a_results) => {
console.log ( `Response ========> ${JSON.stringify(a_results)}.`)
}, (e) => { /* flag error */ }
);
// setPosts(postArr);
// setUrls(newArr);
});
};
Recommendation
Encapsulate the pattern of collecting an array of a predetermined number of promises and firing when they are all settled into a class of their own (or use a Promise/sync library that comes with it).
i'm reading and learning about promises/async/await.
I'm a little stuck trying to understand step by step an error handling method that i already seen a couple of times, its a factory function approach for dealing with errors when creating promises.
Code with comments (questions below)
const myPromise = new Promise((resolve, reject)=>
setTimeout(()=> console.log('ERROR MESSAGE'),1000))
// async / await with error handler factory
const testPromise = async () => {
var data = await myPromise;
console.log(data)
}
// this is where i get stuck
const errorHandlerBig = function(fn){ // (1)
return function(...parameters) { // (2)
return fn(...parameters).catch(function(err){ // (3)
console.error('Caught in Example 3:', err)
})
}
}
errorHandlerBig(testPromise)();
Takes testPromise as argument
I debugged this and parameters content
is [] (empty), when trying to assign a param to testPromise =
async(paramName) => .... i can't work with it inside that function.
what is it trying to spread?
this is translated as testPromise(...params from previous step) i assume
Why is this chain of function calling-inside another function necesary?
Following with item (2), when trying to pass a value to paramName, it doesn't get printed on the console either!:
const testPromise = async (paramName) => {
var data = await myPromise;
console.log(data, paramName)
}
const errorHandlerBig = function(fn){...}
errorHandlerBig(testPromise)('RandomValue')
Thank you everyone in advance!
errorHandlerBig is taking a function and wrapping it in its own error handling.
From the outside in, it:
Takes a function(1) as a parameter.
Creates and returns a new function (2) that accepts any number of parameters.
In function 2, calls function 1 using the parameters passed to function 2, and tries to catch any errors generated.
Since errorHandlerBig is passing the parameters to fn, you would need testPromise to accept the parameters and log them if you expected them to appear in your console. Or you could throw them as an error in testPromise.
As written, this isn't very useful, but it could be useful if you needed to make the same async process run more than once by saving the function returned by errorHandlerBig in a variable and calling that with different parameters, or if you wanted to use the same error handling behavior for multiple asynchronous processes.
const throwThings = async (throwWhat) => {
throw new Error(throwWhat);
}
const happyFunTimes = async (activity) => {
console.log("I'm having fun");
await setTimeout(() => console.log(`jumping on a ${activity}`), 1000);
}
const errorHandlerBig = function(fn){ // (1)
return function(...parameters) { // (2)
return fn(...parameters).catch(function(err){ // (3)
console.error('Caught in Example 3:', err.toString())
});
}
}
document.getElementById(":)").addEventListener("click", () => errorHandlerBig(happyFunTimes)("bananas"));
document.getElementById(">:(").addEventListener("click", () => errorHandlerBig(throwThings)("bananas"));
<button id=":)">Fun times</button>
<button id=">:(">Throw</button>
I'm trying to build a method which reads from firestore an array of elements (object):
I have a service which retrieves the data from firestore, first it gets an array of document references
var data = snapshot.get('elements');
and then it gets all the objects:
getElements(){
return new Promise(res =>{
this.AngularAuth.currentUser
.then( user => {
this.useruid = user.uid;
this.db.firestore.doc(`/users/${this.useruid}`).get().then(snapshot =>{
if(snapshot.exists){
var data = snapshot.get('elements'); //This gets the array of elements
data.forEach(element => {
this.db.firestore.doc(element).get().then(object =>{
if(object.exists){
var elem = object.data() as object;
this.array.push(elem);//I kind of push in the array instances of object
}
else{
console.log("Error. Doc doesn't exist")
}
}).catch(err =>{
console.log(err);
})
});
res(this.array);
}
else{
console.log("Error. Doc doesn't exist")
}
}).catch(function(error) {
// An error happened.
})
})
.catch(function(error) {
// An error happened.
})
});
}
Then in a component I have an async method which calls the service, and tries to push into another array all the names from each object in the first array:
async retrieveArray(){
this.array = await this.service.getElements();
this.array.forEach(element => {
this.names.push(element.name);
});
console.log(this.array);
console.log(this.names);
}
However when I look to the console, the first array (array) gives me indeed an array of objects, but the other array (names) is empty.
I used the method get to retrieve the data because I don't want to listen to it, I might need the value just once.
Personally I find the async/await syntax infinitely more elegant and easier to deal with than a good old .then() callback hell :
async getElements() {
let user;
try{
user = await this.AngularAuth.currentUser();
} catch(err) {
console.log(err);
return;
}
this.useruid = user.uid;
const snapshot = await this.db.firestore.doc(`/users/${this.useruid}`).get();
if (!snapshot.exists) {
console.log("Error. Doc doesn't exist")
return
}
const data = snapshot.get('elements'); //This gets the array of elements
let toReturn = [];
for(let element of data){ // can also use 'await Promise.all()' here instead of for...of
const object = await this.db.firestore.doc(element).get();
toReturn.push(elem);
}
return toReturn;
}
async retrieveArray(){
this.array = await this.service.getElements();
this.names = this.array.map( element => element.name ) // Also use .map() here
console.log(this.array);
console.log(this.names);
}
If you use for...of, all calls will be made one after the other, in order. If you use await Promise.all(), all calls will be made and awaited simultaneously, which is faster but recommended only if you have a small number of calls to make (otherwise this could overload the server you're calling, or even be considered as a DDoS attack.)
I think the issue is in this part of your code:
if(snapshot.exists){
var data = snapshot.get('elements'); //This gets the array of elements
data.forEach(element => {
this.db.firestore.doc(element).get().then(object =>{
if(object.exists){
var elem = object.data() as object;
this.array.push(elem);//I kind of push in the array instances of object
}
else{
console.log("Error. Doc doesn't exist")
}
}).catch(err =>{
console.log(err);
})
});
res(this.nombres);
}
You're looping through the elements and fetching the object from firebase for each one. Each time is an async call, but you're not waiting for each of these calls to finish before calling res(this.nombres).
As for why the console.log(this.array) shows a populated array is that the console can be misleading. It provides the data in a kind of 'live' way (it's a reference to the array), and sometimes by the time the data arrives on the console, it's different to what the data looked like when console.log was called.
To make sure you see the data precisely as it was when console.log was called, try this:
console.log(JSON.parse(JSON.stringify(this.array));
As for the issue with your code, you need to wait for all the elements to have been fetched before you call the resolve function of your promise. Because you don't necessarily know the order in which the responses will come back, one option is to simply have a counter of how many results are remaining (you know how many you are expecting), and once the last response has been received, call the resolve function. This is how I would do it, but obviously I can't test it so it might not work:
if(snapshot.exists){
var data = snapshot.get('elements'); //This gets the array of elements
// *** we remember the number of elements we're fetching ***
let count = data.length;
data.forEach(element => {
this.db.firestore.doc(element).get().then(object =>{
// *** decrement count ***
count--;
if(object.exists){
var elem = object.data() as object;
this.array.push(elem);//I kind of push in the array instances of object
// *** If count has reached zero, now it's time to call the response function
if (count === 0) {
res(this.nombres);
}
}
else{
console.log("Error. Doc doesn't exist")
}
}).catch(err =>{
console.log(err);
})
});
// *** remove this line because it's calling the resolve function before nombres is populated
//res(this.nombres);
}
You might want to add behaviour for when the result of snapshot.get('elements') is empty, but hopefully with this you'll be on your way to a solution.
** EDIT **
I'm keeping this up just because the console.log issue might well be useful for you to know about, but I highly recommend the async/await approach suggested by Jeremy. I agree that's it's much more readable and elegant
I am coding for my React Native app and I am having trouble getting the data from the firebase return outside of the firebase.firestore().collection("test_data").doc(ID) loop. Whenever I check the dataArray variable after the loop it is empty. If I check it within the loop, the data is there. I think that it is a scope problem, but I just do not understand it. I also cannot call any user defined functions inside of the loop.
try {
let dataArray = [];
// get the document using the user's uid
firebase.firestore().collection("users").doc(uid).get()
.then((userDoc) =>{
// if the document exists loop through the results
if (userDoc.exists) {
data = userDoc.data().saved_data; // an array store in firebase
data.forEach(ID => { // loop through array
firebase.firestore().collection("test_data").doc(ID).get()
.then((doc) => {
dataArray.push(doc.data().test_data);
console.log(dataArray) // the data shows
})
console.log(dataArray) // the data does not show
})
}
})
}
catch (error) {
}
}
You're looping through asynchronous calls, so your final console.log will trigger before the data has been received. Your first console.log only triggers after the data has been received.
So the code is working, but the function (promise) will resolve (as undefined, or void) before all the data has been received from your firebase calls.
If you want to return the array to the caller, you could do something like this:
function getDataFromServer() {
// get the document using the user's uid
return firebase.firestore().collection('users').doc(uid).get().then(userDoc => {
// now we're returning this promise
const dataArray = []; // can move this to lower scope
// if the document exists loop through the results
if (userDoc.exists) {
const savedData = userDoc.data().saved_data; // an array store in firebase
return Promise.all(
// wait for all the data to come in using Promise.all and map
savedData.map(ID => {
// this will create an array
return firebase.firestore().collection('test_data').doc(ID).get().then(doc => {
// each index in the array is a promise
dataArray.push(doc.data().test_data);
console.log(dataArray); // the data shows
});
})
).then(() => {
console.log(dataArray);
return dataArray; // now data array gets returned to the caller
});
}
return dataArray; // will always be the original empty array
});
}
Now the function returns the promise of an array, so you could do...
const dataArray = await getDataFromServer()
or
getDataArrayFromServer().then(dataArray => {...})
So I am trying to access thermostatRow and the consts defined within it in another function, and I am trying to test that it is working by logging the const within that function so I know I can break it down into its different parts and push those parts into arrays as needed.
I tried 3 things, 2 of them are in my below code examples and the 3rd one returned Promise { <pending> } but I can't find that example.
const worksheet = workbook.getWorksheet("sheet1");
// worksheet is defined in another function earlier in the file that is called asynchronously before this one.
async function dataFormat(worksheet) {
csvWorkbook = workbook.csv.readFile("./uploads/uploadedFile.csv");
await csvWorkbook.then(async function() {
let restarts = 0;
let nullCounts = true;
let thermostatRows = [];
// don't create arrays of current temp here ! do it in a diff function
// const [serial,date,startDate,endDate,thing3,thing4,thing5] = firstTemps
worksheet.eachRow({ includeEmpty: true }, function(row, rowNumber) {
if (rowNumber > 6) {
thermostatRows.push((row.values));
}
});
thermostatRows.map(thermostatRow => {
[,date,time,SystemSetting,systemMode,calendarEvent,ProgramMode,coolSetTemp,HeatSetTemp,currentTemp,currentHumidity,outdoorTemp,windSpeed,coolStage1,HeatStage1,Fan,DMOffset,thermostatTemperature,thermostatHumidity,thermostatMotion] = thermostatRow
return thermostatRow// Push current temps , heatset and cool set into seperate arrays and use those arrays to run the overheat() function
})
})
};
const dataResult = async (worksheet) => {
let result = await dataFormat(worksheet);
console.log(result);
}
This logs nothing.
So I also tried:
const dataResult = async () => {
let result = await dataFormat();
console.log(result);
}
dataResult()
And got this error:
(node:11059) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'eachRow' of undefined
at /Users/jasonharder/coding/projects/hvacdoctor/controllers/hvacdoctor.js:69:15
at <anonymous>
at process._tickCallback (internal/process/next_tick.js:188:7)
What is happening that I am missing?
Note: I have refactored the code a little bit - I am returning undefined now.
Note: Appended where worksheet is defined (it is defined in the file)