1

I was having some problem with JavaScript promises. Here is my code:

let promiseDataList = new Promise((resolve, reject) => {
// first query to get list of all receipt items under certain category
var query = firebase.database().ref('receiptItemIDsByCategory').child(category);
    query.once( 'value', data => {
        // promises array
        var promises = [];
        
        // loop through each and get the key for each receipt items
        data.forEach(snapshot => {
            var itemData = snapshot.val();
            // get key here
            var itemKey = snapshot.key;

            // second query to get the details of each receipt items based on the receipt item key
            var query = firebase.database().ref('receiptItems').child(itemKey);
            var promise = query.once('value');
            promises.push(promise);

            promise.then(data => { 
                // get receipt item details based on the item key
                var itemDetail = data.val();
                var price = itemDetail.price;
                var quantity = itemDetail.quantity;
                var itemTotal = price * quantity;
                
                var receiptID = itemDetail.receiptID;
                
                // get branch details based on receipt ID
                var query = firebase.database().ref('receipts');
                var promise1 = query.once('value');
                // add promise to array
                promises.push(promise1);
                
                // find matching receiptID then get its branch details
                promise1.then(data => { 
                    data.forEach(snapshot => {
                        
                        snapshot.forEach(childSnapshot => {
                            if(childSnapshot.key == receiptID){
                                var branchDetail = childSnapshot.val().branch;
                                var branchName = branchDetail.branchName;
                                var branchAddress = branchDetail.branchAddress;
                                console.log(branchName + ' ' + branchAddress + ' ' + itemTotal);
                                // push data into the final array to be used in some other parts
                                datasetarr.push({branchName: branchName, branchAddress: branchAddress, total: itemTotal});
                            }
                        });
                        
                    });
                }); 
                    
            });
        }); 
        
        // wait till all promises are finished then resolve the result array
        Promise.all(promises).then(() => resolve(datasetarr)); 
    });             
});

promiseDataList.then((arr) => {
    console.log('promise done');
    for(var i = 0; i < arr.length; i++){
        console.log(arr[i].branchName + ' ' + arr[i].branchAddress + ' ' + arr[i].total);
    }
});

I have added some comments to explain what I am trying to do. The problem now is at the point where I get the receiptID and created another promise variable and push it into promise array, the console.log there is printing out the result.

However, after I commented out that line and tried to print out the result at the .then(), the promise done message is printed out but there is no result from the array, nor error message, which mean the asynchronous execution is not done properly.

Any ideas how to fix this?

vvvvv
  • 25,404
  • 19
  • 49
  • 81
QWERTY
  • 2,303
  • 9
  • 44
  • 85

1 Answers1

1

Nothing in your code is waiting for those subordinate promises. Because you're not returning anything from your then handler, the promise then returns is immediately fulfilled when your handler completes. So you're waiting for the first level promises (the ones you push into promises), but not the ones that query receipt details.

One of the key things about promises is that then returns a new promise; it's a pipeline, where every then (and catch) handler can modify what's passing through. The promise returned by then (or catch) will be fulfilled with the return value of the then handler, if it's not a promise-like ("thenable") value, or will be resolved to the return of the then handler if it's promise-like ("thenable"), meaning that it will be fulfilled or rejected based on what that other promise does. (FWIW, I go into promise terminology — "fulfill" vs. "resolve," etc. — in this post on my blog.)

So you should propagate something from your then handler that waits for the receipts; probably the result of a Promise.all. I'd do it by breaking this up into separate functions (at least two: one that gets the outer level, whatever that is, and one that gets receipts for that level).

Also note that there's no reason to explicitly create a new promise at your top level. You already have one: the one from once.

If I understand what you're trying to do correctly, I've reorganized a bit here to get the receipt information based on the items and then make one call to get the receipts instead of a call per item, and get the branch details from the receipts. See comments:

const promiseDataList =
    // Get all receipt items for the category
    firebase.database().ref('receiptItemIDsByCategory').child(category).once('value').then(itemIds => {
        // Build a map of receipt info keyed by receipt ID by getting all of the items and, as
        // we get them, storing the receipt info in the map
        const receiptMap = new Map();
        return Promise.all(itemIds.map(itemSnapshot => {
            // Get the details of each receipt into a map keyed by receipt ID; since a receipt
            // can have multiple items, be sure to check the map before adding a new entry
            return firebase.database().ref('receiptItems').child(itemSnapshot.key).once('value').then(item => {
                const itemDetail = item.val();
                const price = itemDetail.price;
                const quantity = itemDetail.quantity;
                const itemTotal = price * quantity;
                const receiptEntry = receiptMap.get(itemDetail.receiptID);
                if (receiptEntry) {
                    // We already have this receipt, add to its total
                    receiptEntry.total += itemTotal;
                } else {
                    // New receipt
                    receiptMap.set(itemDetail.receiptID, {id: itemDetail.receiptID, total: itemTotal});
                }
            });
        })).then(() => {
            // We have the map of receipts we want info for; get all receipts so we can filter
            // for only the ones we want. (Note: Surely we could use the keys from receiptMap to
            // limit this Firebase query?)
            return firebase.database().ref('receipts').once('value').then(receipts => {
                // Filter out the irrelevant receipts (again, Firebase perhaps could do that for us?)
                // and then map the remaining ones to objects with the desired information
                return receipts.filter(receipt => receiptMap.has(receipt.key)).map(receipt => {
                    const branchDetail = receipt.val().branch;
                    const branchName = branchDetail.branchName;
                    const branchAddress = branchDetail.branchAddress;
                    console.log(branchName + ' ' + branchAddress + ' ' + receipt.total);
                    return {branchName, branchAddress, total: receipt.total};
                });
            });
        }); 
    }); 

promiseDataList.then(arr => {
    console.log('promise done');
    for (var i = 0; i < arr.length; i++) {
        console.log(arr[i].branchName + ' ' + arr[i].branchAddress + ' ' + arr[i].total);
    }
});

This could be written a bit more concisely with the concise form of arrow functions, but that can sometimes get in the way of clarity.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Sorry but I am new to this and not that familiar with the promise yet. So for the 'some prep' that part, I should replace with query.once and data.forEach for the itemKey part. Then what about the return query which is in the third line of the solution? – QWERTY Aug 01 '17 at 11:02
  • @DeniseTan: Actually, the "some prep" would be things like `var itemData = snapshot.val(); var itemKey = snapshot.key;` and such. `firebase...once()` would be the `query` part. If you're not that familiar with promises, I strongly recommend working through some simpler scenarios first and building up to this task. – T.J. Crowder Aug 01 '17 at 11:07
  • Hey after I get the receiptID what should I do based on your solution? I got stuck at the mapping and last return statement there. I have updated the question, do you mind to help me to take a look? – QWERTY Aug 01 '17 at 11:55
  • @DeniseTan: Again: I recommend working through simple examples and building up to the relatively complex task above. – T.J. Crowder Aug 01 '17 at 11:58
  • Or maybe I could learn from this example. My previous example was working but it stopped at the receiptID there whereby I do not have a nested promise and I just simply resolve from there. But for this example, it required another promise from there but somehow it leads to the forgotten promise problem & I not sure how to actually resolve that. – QWERTY Aug 01 '17 at 12:03
  • @DeniseTan: As I said above, `query` would represent the query to Firebase, returning the promise you get from `once`. So you just apply that to the code you've put in your question, returning the promise from `once`. Similarly, you'll need to use `Promise.all` within that query to map the child requests to promises and then transform the result with `then`. Obviously I can't write the code for you, and I don't think you want me to; I've updated the answer with an example, though, of getting parent items and their subordinate children, waiting for the overall result. – T.J. Crowder Aug 01 '17 at 12:26
  • I got error message firebase.database(...).ref(...).child(...).then is not a function from the first return statement. Thanks for the help though! – QWERTY Aug 01 '17 at 12:37
  • @DeniseTan: I don't know Firebase, but don't you need `.once('value')` between `child(...)` and `.then`? – T.J. Crowder Aug 01 '17 at 12:38
  • Opps sorry that was my mistake. But for now from the edited portion, the itemKey is no longer looping and gets only one record. It's all in the mess now :( – QWERTY Aug 01 '17 at 12:44
  • @DeniseTan: Actually, my example at the end may not be all that helpful, because I think you're doing something slightly different. Could you add a simple, clear statement to the question saying what you're doing? Something along the lines of "From the receipts in a category, I need to get an array of the branch details for those receipts." Also, you don't need to repeat the `firebase.database().ref('receipts')` call, do you? It doesn't seem to have any parameters, and you're having to look for the receipt in the resulting array, so... – T.J. Crowder Aug 01 '17 at 12:59
  • Sorry for my bad explanations :( I have updated the questions with comments. What I am trying to do is get list of receipt items under certain category, then for each receipt item, get its details like price, quantity & receiptID. After I get the receptID, I need to find matching receiptID, if matched, I get the branch details. After I get everything, I simply add the receipt item details and branch details to the finalized array. – QWERTY Aug 01 '17 at 13:07
  • As for the repeat call of firebase.database().ref('receipts'), I actually need because my firebase structure is receipts -> accountID -> receiptID. So what I trying to do is get all receipts record, loop through all accounts, then under each accounts, I find the matching receiptID to get its branch details. And then I proceed to add the data to the finalized array – QWERTY Aug 01 '17 at 13:09
  • @DeniseTan: I don't think you do need to do it repeatedly. :-) I think I understand now and have taken a stab at it; see the (new) end of the answer. – T.J. Crowder Aug 01 '17 at 13:27
  • Thanks so much for the helps! But I am getting itemIds.map is not a function error message. I think it is something related to firebase as JaromandaX previously has encountered this .map problem also. – QWERTY Aug 01 '17 at 13:31
  • @DeniseTan: If the first `data.forEach` in your code doesn't fail, the `itemIds.map` in my code shouldn't fail. Both `forEach` and `map` are methos of arrays. Unless what we're getting has `forEach` but **isn't** an array. At that point you're into Firebase-land and I can't help you. If it's array-like, you can transform it into an array first via `Array.from` (modern, but can be polyfilled). In any case, I think the above should get you headed the right way. – T.J. Crowder Aug 01 '17 at 13:43
  • I see I see. Thanks so much for the helps. I actually managed to print out result from the inside but not the .then(), so I think the data fetching should not be a problem. I think the problem is that the forEach isn't an array. – QWERTY Aug 01 '17 at 13:51