85

Seem to be having some issues incorporating async/await with .reduce(), like so:

const data = await bodies.reduce(async(accum, current, index) => {
  const methodName = methods[index]
  const method = this[methodName]
  if (methodName == 'foo') {
    current.cover = await this.store(current.cover, id)
    console.log(current)
    return {
      ...accum,
      ...current
    }
  }
  return {
    ...accum,
    ...method(current.data)
  }
}, {})
console.log(data)

The data object is logged before the this.store completes...

I know you can utilise Promise.all with async loops, but does that apply to .reduce()?

Bergi
  • 513,640
  • 108
  • 821
  • 1,164
benhowdle89
  • 34,076
  • 63
  • 192
  • 314

7 Answers7

160

The problem is that your accumulator values are promises - they're return values of async functions. To get sequential evaluation (and all but the last iteration to be awaited at all), you need to use

const data = await array.reduce(async (accumP, current, index) => {
  const accum = await accumP;
  …
}, Promise.resolve(…));

That said, for async/await I would in general recommend to use plain loops instead of array iteration methods, they're more performant and often simpler.

Community
  • 1
  • 1
Bergi
  • 513,640
  • 108
  • 821
  • 1,164
  • 3
    Thanks for your advice at the end. I ended up using just a plain for loop for what I was doing, and it was the same lines of code, but far easier to read... – cs_pupil Oct 26 '17 at 21:24
  • 3
    The `initialValue` of the `reduce` does not need to be a `Promise`, it will however in most cases clarify the intent. – EECOLOR Nov 13 '17 at 21:05
  • @EECOLOR It should be, though. I really dislike `await` having to cast a plain value into a promise – Bergi Nov 14 '17 at 11:26
  • 1
    @EECOLOR And when using TypeScript, the initial value needs to be a promise, because the return type of the callback must always match the type of the accumulator. – jessedvrs Mar 23 '18 at 10:38
  • @jessedvrs I think you mean the initial value (if not I probably misunderstand what you are saying). You could pass `null` couldn't you? – EECOLOR Mar 23 '18 at 19:31
6

I like Bergi's answer, I think it's the right way to go.

I'd also like to mention a library of mine, called Awaity.js

Which lets you effortlessly use functions like reduce, map & filter with async / await:

import reduce from 'awaity/reduce';

const posts = await reduce([1,2,3], async (posts, id) => {

  const res = await fetch('/api/posts/' + id);
  const post = await res.json();

  return {
    ...posts,
    [id]: post
  };
}, {})

posts // { 1: { ... }, 2: { ... }, 3: { ... } }
Asaf Katz
  • 4,152
  • 4
  • 35
  • 41
2

You can wrap your entire map/reduce iterator blocks into their own Promise.resolve and await on that to complete. The issue, though, is that the accumulator doesn't contain the resulting data/object you'd expect on each iteration. Due to the internal async/await/Promise chain, the accumulator will be actual Promises themselves that likely have yet to resolve themselves despite using an await keyword before your call to the store (which might lead you to believe that the iteration won't actually return until that call completes and the accumulator is updated.

While this is not the most elegant solution, one option you have is to move your data object variable out of scope and assign it as a let so that proper binding and mutation can occur. Then update this data object from inside your iterator as the async/await/Promise calls resolve.

/* allow the result object to be initialized outside of scope 
   rather than trying to spread results into your accumulator on iterations, 
   else your results will not be maintained as expected within the 
   internal async/await/Promise chain.
*/    
let data = {}; 

await Promise.resolve(bodies.reduce(async(accum, current, index) => {
  const methodName = methods[index]
  const method = this[methodName];
  if (methodName == 'foo') {
    // note: this extra Promise.resolve may not be entirely necessary
    const cover = await Promise.resolve(this.store(current.cover, id));
    current.cover = cover;
    console.log(current);
    data = {
      ...data,
      ...current,
    };
    return data;
  }
  data = {
    ...data,
    ...method(current.data)
  };
  return data;
}, {});
console.log(data);
Brandon K
  • 703
  • 1
  • 4
  • 12
1

[Not addressing OPs exact prob; focused on others who land here.]

Reduce is commonly used when you need the result of the previous steps before you can process the next. In that case, you can string promises together a la:

promise = elts.reduce(
    async (promise, elt) => {
        return promise.then(async last => {
            return await f(last, elt)
        })
    }, Promise.resolve(0)) // or "" or [] or ...

Here's an example with uses fs.promise.mkdir() (sure, much simpler to use mkdirSync, but in my case, it's across a network):

const Path = require('path')
const Fs = require('fs')

async function mkdirs (path) {
    return path.split(/\//).filter(d => !!d).reduce(
        async (promise, dir) => {
            return promise.then(async parent => {
                const ret = Path.join(parent, dir);
                try {
                    await Fs.promises.lstat(ret)
                } catch (e) {
                    console.log(`mkdir(${ret})`)
                    await Fs.promises.mkdir(ret)
                }
                return ret
            })
        }, Promise.resolve(""))
}

mkdirs('dir1/dir2/dir3')

Below is another example which add 100 + 200 ... 500 and waits around a bit:

async function slowCounter () {
    const ret = await ([100, 200, 300, 400, 500]).reduce(
        async (promise, wait, idx) => {
            return promise.then(async last => {
                const ret = last + wait
                console.log(`${idx}: waiting ${wait}ms to return ${ret}`)
                await new Promise((res, rej) => setTimeout(res, wait))
                return ret
            })
        }, Promise.resolve(0))
    console.log(ret)
}

slowCounter ()
ericP
  • 1,130
  • 12
  • 17
1

Sometimes the best thing to do is simply put both code versions side by side, sync and async:

Sync version:

const arr = [1, 2, 3, 4, 5];

const syncRev = arr.reduce((acc, i) => [i, ...acc], []); // [5, 4, 3, 2, 1] 

Async one:

(async () => { 
   const asyncRev = await arr.reduce(async (promisedAcc, i) => {
      const id = await asyncIdentity(i); // could be id = i, just stubbing async op.
      const acc = await promisedAcc;
      return [id, ...acc];
   }, Promise.resolve([]));   // [5, 4, 3, 2, 1] 
})();

//async stuff
async function asyncIdentity(id) {
   return Promise.resolve(id);
}

const arr = [1, 2, 3, 4, 5];
(async () => {
    const asyncRev = await arr.reduce(async (promisedAcc, i) => {
        const id = await asyncIdentity(i);
        const acc = await promisedAcc;
        return [id, ...acc];
    }, Promise.resolve([]));

    console.log('asyncRev :>> ', asyncRev);
})();

const syncRev = arr.reduce((acc, i) => [i, ...acc], []);

console.log('syncRev :>> ', syncRev);

async function asyncIdentity(id) {
    return Promise.resolve(id);
}
l30_4l3X
  • 81
  • 2
  • 4
0

export const addMultiTextData = async(data) => {
  const textData = await data.reduce(async(a, {
    currentObject,
    selectedValue
  }) => {
    const {
      error,
      errorMessage
    } = await validate(selectedValue, currentObject);
    return {
      ...await a,
      [currentObject.id]: {
        text: selectedValue,
        error,
        errorMessage
      }
    };
  }, {});
};
  • 3
    While this code snippet may solve the question, [including an explanation](//s.tk/meta/questions/114762/explaining-entirely-code-based-answers) really helps to improve the quality of your post. Remember that you are answering the question for readers in the future, and those people might not know the reasons for your code suggestion. – Shree Apr 17 '19 at 10:25
  • Not to say I wouldn't even recommend this approach since using spread operators in loops is very performance heavy. – Robert Molina Jul 01 '20 at 16:38
-1

Here's how to make async reduce:

async function asyncReduce(arr, fn, initialValue) {
  let temp = initialValue;

  for (let idx = 0; idx < arr.length; idx += 1) {
    const cur = arr[idx];

    temp = await fn(temp, cur, idx);
  }

  return temp;
}
Wojciech Maj
  • 813
  • 6
  • 17