0

I'm trying to set random number to all fruits in array and return an object containing all the fruits with async function in reducer, but only the last element of array is returned

(async () =>
  console.log(
    await [("apple", "banana", "orange")].reduce(async (o, fruit) => {
      const random_number = await new Promise((resolve) => setTimeout(() => resolve(Math.random()), 200));
      return { ...o, [fruit]: random_number };
    }, {})
  ))();
Slinidy
  • 367
  • 1
  • 4
  • 12
  • 2
    `.reduce()` runs synchronously. The async functions it launches, only finish their execution *after* `.reduce()` has finished. – VLAZ Oct 14 '21 at 15:02
  • 2
    Checkout the answer on this thread. https://stackoverflow.com/questions/41243468/javascript-array-reduce-with-async-await – Shahbaz Shueb Oct 14 '21 at 15:05
  • That's just not how that works at all. You'd need something like `await Promise.all([..].map(async (f) => { const r = await ...; return [f, r]; }))` to give you a list of fruit-number pairs, which you can *then* `reduce` to one object. – deceze Oct 14 '21 at 15:06
  • 2
    Apart from other answers, your array definition is wrong `[("apple", "banana", "orange")]` simply evaluates to `["orange"]`. It should be: `["apple", "banana", "orange"]`. – XCS Oct 14 '21 at 15:07
  • You should also log `o` and `fruit` to see what's wrong. `o` won't be an accumulator, it will be the promise (async function itself), so when you spread it `{...o}` you get an empty object `{}`. At the end it's simply `{...{}, "orange": rand()}`. – XCS Oct 14 '21 at 15:09
  • @deceze that is inaccurate. there's only a minor mistake with the way `reduce` is being used in the post – Mulan Oct 14 '21 at 15:11
  • @EamonnMcEvoy I think because there were multiple issues, especially the array creation which seems more like a typo. – XCS Oct 14 '21 at 15:39

2 Answers2

5

You're very close. The main issue is that you wrote the array as [(...)] instead of [...]. In your code, the array only contains one value, if you remove the () you will have three values. See comma operator for details.

The other thing you have to consider is that an async function always returns a promise, so you must await o as well -

(async () =>
  console.log(
    await ["apple", "banana", "orange"].reduce(async (o, fruit) => {
      const rand = new Promise((resolve) => setTimeout(() => resolve(Math.random()), 200))
      return { ...await o, [fruit]: await rand }
    }, {})
  ))();
{
  "apple": 0.8731611352383968,
  "banana": 0.163521266739585,
  "orange": 0.8419246752641664
}

Note reduce as used above will resolve the promises in series (sequentially). Other comments suggesting map->Promise.all will resolve the promises in parallel (simultaneously)

Mulan
  • 129,518
  • 31
  • 228
  • 259
  • Interesting that this works, a very confusing way to do reduce to be honest. The main gist is that `async () => {}` returns a `Promise`, so the accumulator itself is a promise. – XCS Oct 14 '21 at 15:13
  • there are other ways to write this but it shouldn't be too surprising that this works. ultimately `reduce` folds many values into single value so it's only a matter of sequencing the promises to combine them. – Mulan Oct 14 '21 at 15:19
  • Actually, the thing that is weird is that the accumulator type seems different from the return type. Because we simply return an object `return {...}` but the incoming accumulator parameter is a `Promise`. The trick is that even though we return a simple object, our entire return statement is inside an async function which indeed wraps it in a Promise. So at each iteration we receive `Promise` and return `Promise`. – XCS Oct 14 '21 at 15:22
  • @XCS it's a bit of a side-effect but valid. The main issue here is that it's mixing sync/async values for the accumulator, since the initial value is a plain `{}`, the next one is a promise for an object. So, `reduce` returns a promise for a value anyway, however, OP's doesn't acknowledge that the accumulator is a promise (sometimes). – VLAZ Oct 14 '21 at 15:22
  • @VLAZ Good point on that the initial value is an empty object `{}`. Weird that we can do `...await {}`. This works: `(async () => {console.log({...await {}});})();`. – XCS Oct 14 '21 at 15:24
  • Relevant answer: https://stackoverflow.com/a/55263084/407650 – XCS Oct 14 '21 at 15:27
  • 1
    @XC5 correct, we see `return { ... }` but in a `async` function it's automatically wrapped in a promise. however the programmer has to be aware of this anywhere `async` functions are used. and yes, `await ...` can resolve _any_ value, not only promises. – Mulan Oct 14 '21 at 15:29
  • 1
    @XCS `await` will always pause and wait until the next value is resolved. If used with a non-promise, `await x` acts like `await Promise.resolve(x)`. – VLAZ Oct 14 '21 at 15:33
1

If you add another console log in the reducer you can see what is happening.

(async () =>
  console.log(
    await [("apple", "banana", "orange")].reduce(async (o, fruit) => {
      console.dir({o, fruit})
      const random_number = await new Promise((resolve) => setTimeout(() => resolve(Math.random()), 200));
      return { ...o, [fruit]: random_number };
    }, {})
  ))();

output:

{ o: {}, fruit: 'orange' }
{ orange: 0.39647395383957074 }

This is because you have wrapped your array item in parentheses [("apple", "banana", "orange")] so there is only one item to reduce.

After fixing this the output is

{ o: {}, fruit: 'apple' }
{ o: Promise { <pending> }, fruit: 'banana' }
{ o: Promise { <pending> }, fruit: 'orange' }
{ orange: 0.8347447113637538 }

Now you can see that the promises are not properly awaited. @Mulan already gave the answer for this so I won't repeat it.

I will suggest using map instead of reduce for this. This will stop the promises from blocking one another, in your reduce code the reducer must wait for one promise to complete before moving the the next. If you gather all the promises together and await them using Promise.all you can avoid this.

  (async () => {
    //map the array to an array of promises
    const promises = ["apple", "banana", "orange"].map(async (fruit) => {
      const random_number = await new Promise((resolve) => setTimeout(() => resolve(Math.random()), 200));
      return { [fruit]: random_number };
    });

    //wait for all promises to resolve
    const results = await Promise.all(promises);

    //merge the result objects together
    const result = Object.assign(...results);
    
    console.log(result)
  })();
Eamonn McEvoy
  • 8,876
  • 14
  • 53
  • 83