-2

I'm getting some unusual behaviour.

Basically as part of my code I have a function which utilizes nested for loops to build a promise and add it to a list of promises.

After the nested loops are complete, I'd like to evaluate the promise list using promise.all().

I have successfully managed to do this with a single forEach loop in the past, the nesting seems to cause some issues, namely, testing reveals that the Promise.all is being called before the nested forEach loop terminates, resulting in it being called on an empty list and hence returning an empty list.

I have a feeling that the issue is that I'm missing a return statement somewhere in my nested forEach loop as mentioned in this answer but I have not been able to determine where.

culprit.js
const otherModule = require("blablabla")
const otherOtherModule = require("blablabla2")

function nestedFunction(list){
  var promises = [];
  list.forEach(element => {
      otherModule.getSublist(element).then(sublist => {
          sublist.forEach(subelement => {
              promises.push(otherOtherModule.promiseResolvingFunction(subelement));
          });
      });
  });
  return Promise.all(promises);
}

module.exports = {
  nestedFunction : nestedFunction  
}
culprit.test.js
const culprit = require("culpritpath")
// for mocking
const otherModule = require("blablabla")
otherModule.getSublist = jest.fn(() => Promise.resolve([{}, {}, {}]))
const otherOtherModule = require("blablabla2")
otherOtherModule.promiseResolvingFunction = jest.fn(() => Promise.resolve())

describe("nestedFunction()", ()=>{
  it("returns an array of resolved promises", () => {
      return culprit.nestedFunction([{}, {}]).then(res => {
          expect(res).toHaveLength(6);
      })
  })
})

instead I get that res is []. Further tests show that promiseResolvingFunction is being called the right number of times, so as I understand, Promise.all is being called before the nested forEach loop finishes.


PS: I am still getting started with promises and TDD, I am more than happy to hear feedback on any code smell.

skyboyer
  • 22,209
  • 7
  • 57
  • 64
thesofakillers
  • 290
  • 3
  • 13
  • 1
    The issue is that your `.push()` call is nested within another promise resolution. So your outer `.forEach()` completes before any of those `.then()`s execute, and your promises array is still empty when you pass it to `Promise.all()` – Robb Traister Oct 10 '19 at 20:14
  • Exactly, `Promise.all(promises);` is being called outside of `.then`. So before `.then` even executes you're already calling `Promise.all` – Eddie D Oct 10 '19 at 20:19
  • Hi all, thank you, I have selected a solution. May I ask why all solutions opted for `map` rather than `forEach`? – thesofakillers Oct 10 '19 at 20:54

3 Answers3

2

Yeah so the problem I see is that your for each loop is calling asynchronous code and expecting it to execute synchronously.

I'd probably do something like...

var promises = list.map(element => {
    return otherModule.getSublist(element).then(sublist => {

        // Map all of the sublists into a promise
        return Promise.all(sublist.map(subelement => {
            return otherOtherModule.promiseResolvingFunction(subelement));
        }));
    });
});
return Promise.all(promises);

Of course then you'd end up with an array of arrays. If you wanted to keep the result a flat array of sublist items, another option would be to first fetch all of your lists, then fetch all of your sublists from those results...

return Promise.all(list.map( element => otherModule.getSublist(element)))
  .then((sublists) => {
    let subListPromises = [];

    // Loop through each sublist, turn each item in it into a promise
    sublists.forEach( sublist => {
        sublistPromises = [
          ...sublistPromises, 
          sublist.map( subelement => otherOtherModule.promiseResolvingFunction(subelement))
        ]
    })

    // Return a promise dependent on *all* of the sublist elements
    return Promise.all(sublistPromises)
  })
1

You execute Promise.all before the array has been populated (which happens asynchronously).

It may look difficult to deal with nested promises, but just apply Promise.all to the inner arrays of promises, and then at the outer level, apply Promise.all to all those from the inner level.

Then you're not ready yet, as now you have a promise that resolves to an array of arrays (corresponding to the originally nested promises), so you need to flatten that with the quite new .flat method, or with [].concat:

function nestedFunction(list) {
    // Get promise for the array of arrays of sub values
    return Promise.all(list.map(element => {
        return otherModule.getSublist(element).then(sublist => {
            // Get promise for the array of sub values
            return Promise.all(sublist.map(subelement => {
                return otherOtherModule.promiseResolvingFunction(subelement);
            }));
        });
    })).then(matrix => [].concat(...matrix)); // flatten the 2D array
}
trincot
  • 317,000
  • 35
  • 244
  • 286
-2

You need to nest your promise resolution. Something like this:

const otherModule = require("blablabla")
const otherOtherModule = require("blablabla2")

function nestedFunction(list){
  var promises =
  list.map(element => {
      return otherModule.getSublist(element).then(sublist => {
          return Promise.all(
            sublist.map(subelement => {
              return otherOtherModule.promiseResolvingFunction(subelement);
            })
         );
      });
  });
  return Promise.all(promises);
}

module.exports = {
  nestedFunction : nestedFunction  
}
  • Hi thank you for your answer. I'm afraid this is making it so that promises is `undefined`. I checked by adding a console.log(promises) just before the final return statement. Could it be that this is due to overly simplistic mocking on my behalf? Btw someone came and downvoted all questions/comments, was not me. Thank you. – thesofakillers Oct 10 '19 at 20:42
  • that's what I get for writing code on my phone. Edited to add missing return in first map – Robb Traister Oct 10 '19 at 23:55