user6269972
user6269972

Reputation: 319

Why is Promise.all needed for Array.map Array.forEach asynchronous operation?

I am using NodeJS (ExpressJS) server, have the following code:

let caseScore = 0
await questions.map(async q => {
  caseScore += await q.grade() // Queries database and gets questions, finds score
}))
console.log(caseScore)
>> Output: 0

However, it appears that q.grade() finishes executing after the request is finished. I put a console.log() within q.grade, and it shows up after response is sent. Clearly this is executed asynchronously. So later I did this:

let caseScore = 0
await Promise.all(questions.map(async q => {
  caseScore += await q.grade() // Queries database and gets questions, finds score
})))
console.log(caseScore)
>> Output: 2

It works perfectly. Can someone explain to me why Promise.all is needed? Also, if I switch .map for .forEach, Promise.all errors, what is the correct way to do this for .forEach?

Upvotes: 2

Views: 938

Answers (3)

zeachco
zeachco

Reputation: 780

When you await an array of promises, you wait for an array which is not a promise (even though it contain promises inside). await will immediately resolve values that are not a Promise. It's like if you did await 'hello', that would be resolving instantly.

Promise.all is a util that exposes a new Promise that resolves only when all the promises in the array passed as an argument are resolved. Since it's creates it's own promise, you can await on Promise.all.

[EDIT] Careful, do not use await in loops like this

for (var q of questions) {
  caseScore += await q.grade();
}

The reason is that it translates to

questions[0].grade().then(score => 
  return questions[1].grade().then(score => 
    return questions[2].grade().then(score => 
      return questions[3].grade().then(score => 
        // ...
      );
    );
  );
);

it creates a lots of context in memory and makes everything serial which is not taking advantage of javascript async nature

if you want to have a sum or some values shared between promises you can use a context object

async function grade1() {
  this.sum += await getSumFromServer();
  this.nbOfTasks += 1;
}

async function grade2() {
  this.sum += await getSumFromServer();
  this.nbOfTasks += 1;
}

async function grade3() {
  this.sum += await getSumFromServer();
  this.nbOfTasks += 1;
}

async function main() {
  const context = {
    sum: 0,
    nbOfTasks: 0,
  }
  
  const promisesWithContext = [grade1, grade2, grade3]
    .map(fn => fn.call(context));

  await Promise.all(promisesWithContext);
  
  console.log(context);
}

main();

// STUB
function getSumFromServer() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(Math.random() * 100)
    }, 1000)
  });
}

Upvotes: 3

TheMisir
TheMisir

Reputation: 4279

Array.prototype.map works like that.

Array.prototype.map = function(callback) {
  var results = [];
  for (var i = 0; i < this.length; i++) {
    results.push(callback(this[i], i));
  }
  return result;
}

In your code your callback returns Promise and Array.map returns array of Promise. Because it doesn't awaits for callback to be completed. Array.forEach also will not work for you because it doesn't awaits callback too.

The best way to solve your problem is using Promise.all to convert array of Promise into a single Promise and await it.

await Promise.all(questions.map(async q => {
  caseScore += await q.grade();
}));

Or using simple for loop like that.

for (var q of questions) {
  caseScore += await q.grade();
}

Upvotes: 2

user13505400
user13505400

Reputation:

You should have to wait for all async calls to be completed. That is the reason why promise.all needed.

It converts the set of promises to a single promise.

Upvotes: 4

Related Questions