3

I have an array of objects and I have to add one property on each of the objects coming from and async function

I am doing an Array.reduce to iterate on each of the elements and return just one result: One array of objects with the new property.

I have this

const res = await resultOne.reduce(async (users = [], user, i) => {
          let userName;
          try {
            let { name } = await names.getNames(user.id);
            userName = name;
          } catch (error) {
            throw error;
          }
          delete user.id;
          users.push({ ...user, userName });
          return users;
      }, []);

But I get the message

Push is not a function of users

And this is because I think is a promise.

How can I handle async requests in a reduce or a map

Heretic Monkey
  • 11,687
  • 7
  • 53
  • 122
Ruben Saucedo
  • 239
  • 1
  • 13
  • 1
    If you can use a Promise library, you can use bluebird's Promise.reduce for this. I.E. `const res = await Promise.reduce( resultOne, async (users ...`. – Paul Aug 14 '19 at 20:06
  • @Paulpro thank you for your response, the thing is that i don't think that add another dependency is completely necessary for this case, this is something that I won't be repeating in the whole project. – Ruben Saucedo Aug 14 '19 at 22:08

2 Answers2

8

Yes, users is a promise. Don't use reduce with async functions. You could do something like await users in the first line, but that would be pretty inefficient and unidiomatic.

Use a plain loop:

const users = [];
for (const user of resultOne) {
    const { name } = await names.getNames(user.id);
    delete user.id;
    users.push({ ...user, userName: user });
}

or, in your case where you can do everything concurrently and create an array anyway, the map function together with Promise.all:

const users = await Promise.all(resultOne.map(async user => {
    const { name } = await names.getNames(user.id);
    delete user.id;
    return { ...user, userName: user };
}));
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Thank you for your response @Bergi, I think the plain loop could work great and is even more readable, the number of users that will be passed through the loop are thousands. But I don't think that there is a way to avoid running all the loop to delete the user.id and add the user.name – Ruben Saucedo Aug 14 '19 at 22:14
1

Because it's an async function, every time you return something it gets wrapped in a promise. To fix this you need to set the starting array as a promise and then await for the accumulator on each iteration.

const res = await resultOne.reduce(async (users, user, i) => {
  try {
    return [
      ...await users,
      { ...user, userName: await names.getNames(user.id.name) }
    ]
  } catch (error) {
    console.log(error)
  }
}, Promise.resolve([]));
Joss Classey
  • 1,054
  • 1
  • 7
  • 22
  • `await`ing the `users` later (after `await`ing the `getNames()`) will cause unhandled rejection warnings if one of the promises fails, see https://stackoverflow.com/questions/46889290/waiting-for-more-than-one-concurrent-await-operation or https://stackoverflow.com/questions/45285129/any-difference-between-await-promise-all-and-multiple-await – Bergi Aug 14 '19 at 20:05
  • 2
    @Bergi I just updated my answer. Would this prevent/fix what you're talking about? – Joss Classey Aug 14 '19 at 20:09
  • 1
    Yes, they would be executed sequentially now. Still I don't think it's a good solution, it's confusing and easy to mess up :-) – Bergi Aug 14 '19 at 20:11
  • Thank you @JossClassey for your response, I was following kind of this solution but I also agree that is not the more readable option. – Ruben Saucedo Aug 14 '19 at 22:13