5

Let me first show you what the code looks like

 Cart.getCompleteCart((cart)=>{
    
       let products = []; *//this Array has to be filled*

       totalPrice = cart.totalPrice;

       for(let prod of cart.products){

                Product.getProductDetails(prod.productId, productDetails => {
                    products.push({
                        product: productDetails,
                        productCount: prod.productCount
                    });
                });

      }
      console.log("Products are ->", products); *//this line is running before for loop!*
 }
       

console.log() is running before the for loop completes its work.

How to run "for loop" kind of in sync with the console.log() ?

Ashutosh Tiwari
  • 1,496
  • 11
  • 20
  • Does this answer your question? [JavaScript closure inside loops – simple practical example](https://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example) – Liam Jul 21 '20 at 14:31
  • Also see [How do I return the response from an asynchronous call?](https://stackoverflow.com/questions/14220321/how-do-i-return-the-response-from-an-asynchronous-call) – Liam Jul 21 '20 at 14:34
  • Can you answer it in context of what I have done? It would be really helpful. – Ashutosh Tiwari Jul 21 '20 at 14:34
  • One option would be to use a callback, i.e. a function that you would call from within the closure after it is finished. A different option would be to use a Promise. – Dirk R Jul 21 '20 at 14:34
  • `products.push` happens **after** `console.log` because async – Liam Jul 21 '20 at 14:34

2 Answers2

9

I assume that Product.getProductDetails() is an async method (it looks like a query to an API).

console.log() is not "running before the for loop", it is just that the tasks performed by Product.getProductDetails() are being handled asynchronously.

Since it also seems that the Product.getProductDetails() method works with callbacks, one way to achieve your goal would be to promisify each call and then condensate all the promises into a single one by using Promise.all() method.

Something like this should do the trick:

Cart.getCompleteCart((cart) => {
    const promises = [];

    for (let prod of cart.products) {
        promises.push(
            new Promise((resolve) => {
                Product.getProductDetails(prod.productId, (productDetails) => {
                    resolve({
                        product: productDetails,
                        productCount: prod.productCount
                    });
                });
            })
        );
    }

    Promise.all(promises).then((products) => {
        console.log('Products are ->', products);
    });
});

Or:

Cart.getCompleteCart((cart) => {
    Promise.all(
        cart.products.map((prod) => {
            return new Promise((resolve) => {
                Product.getProductDetails(prod.productId, (productDetails) => {
                    resolve({
                        product: productDetails,
                        productCount: prod.productCount
                    });
                });
            });
        })
    ).then((products) => {
        console.log('Products are ->', products);
    });
});
Ernesto Stifano
  • 3,027
  • 1
  • 10
  • 20
  • Don't use `products.push` though, just resolve the promise with that object. `Promise.all` will fulfill with the desired result. – Bergi Jul 21 '20 at 14:43
  • 1
    @Bergi You are right, I know. I just didn't want to change the code a lot to make it more didactic. I will update my answer accordingly. – Ernesto Stifano Jul 21 '20 at 14:45
2

Promise.all is designed for pretty much this exact use-case:

// A dummy "Product" with a dummy "getProductDetails" implementation
// so that we can have a working test example
let Product = {
  getProductDetails: (productId, callback) => {
    setTimeout(() => {
      callback({ type: 'productDetails', productId });
    }, 100 + Math.floor(Math.random() * 200));
  }
};

// This is the function you're looking for:
let getCompleteCart = async (cart) => {
  
  return Promise.all(cart.products.map(async ({ productId, productCount }) => ({
    product: await new Promise(resolve => Product.getProductDetails(productId, resolve)),
    productCount
  })));
  
}

let exampleCart = {
  products: [
    { productId: 982, productCount: 1 },
    { productId: 237, productCount: 2 },
    { productId: 647, productCount: 5 }
  ]
};
getCompleteCart(exampleCart).then(console.log);

A breakdown of getCompleteCart:

let getCompleteCart = async (cart) => {
  
  return Promise.all(cart.products.map(async ({ productId, productCount }) => ({
    product: await new Promise(resolve => Product.getProductDetails(productId, resolve)),
    productCount
  })));
  
}

Promise.all (mdn) is purpose-built to wait for every promise in an Array of promises to resolve

We need to supply an Array of Promises to Promise.all. This means we need to convert our Array of data (cart.products) to an Array of Promises; Array.protoype.map is the perfect tool for this.

The function provided to cart.products.map converts every product in the cart to an Object that looks like { product: <product details>, productCount: <###> }.

The tricky thing here is getting the value for the "product" property, since this value is async (returned by a callback). The following line creates a promise that resolves to the value returned by Product.getProductDetails, using the Promise constructor (mdn):

new Promise(resolve => Product.getProductDetails(productId, resolve))

The await keyword allows us to convert this promise into the future value it results in. We can assign this future value to the "product" attribute, whereas the "productCount" attribute is simply copied from the original item in the cart:

{
  product: await new Promise(resolve => Product.getProductDetails(productId, resolve)),
  productCount
}

In order to run console.log at the right time we need to wait for all product details to finish compiling. Fortunately this is handled internally by getCompleteCart. We can console.log at the appropriate time by waiting for getCompleteCart to resolve.

There are two ways to do this:

Using Promise.prototype.then (mdn):

getCompleteCart(exampleCart).then(console.log);

Or, if we're in an async context we can use await:

let results = await getCompleteCart(exampleCart);
console.log(results);
Gershom Maes
  • 7,358
  • 2
  • 35
  • 55