QWERTY
QWERTY

Reputation: 2315

JavaScript multiple promises resulting in null

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?

Upvotes: 1

Views: 137

Answers (1)

T.J. Crowder
T.J. Crowder

Reputation: 1074335

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.

Upvotes: 1

Related Questions