2999

Are there any issues with using async/await in a forEach loop? I'm trying to loop through an array of files and await on the contents of each file.

import fs from 'fs-promise'

async function printFiles () {
  const files = await getFilePaths() // Assume this works fine

  files.forEach(async (file) => {
    const contents = await fs.readFile(file, 'utf8')
    console.log(contents)
  })
}

printFiles()

This code does work, but could something go wrong with this? I had someone tell me that you're not supposed to use async/await in a higher-order function like this, so I just wanted to ask if there was any issue with this.

BinaryButterfly
  • 18,137
  • 13
  • 50
  • 91
Saad
  • 49,729
  • 21
  • 73
  • 112

33 Answers33

5353

Sure the code does work, but I'm pretty sure it doesn't do what you expect it to do. It just fires off multiple asynchronous calls, but the printFiles function does immediately return after that.

Reading in sequence

If you want to read the files in sequence, you cannot use forEach indeed. Just use a modern for … of loop instead, in which await will work as expected:

async function printFiles () {
  const files = await getFilePaths();

  for (const file of files) {
    const contents = await fs.readFile(file, 'utf8');
    console.log(contents);
  }
}

Reading in parallel

If you want to read the files in parallel, you cannot use forEach indeed. Each of the async callback function calls does return a promise, but you're throwing them away instead of awaiting them. Just use map instead, and you can await the array of promises that you'll get with Promise.all:

async function printFiles () {
  const files = await getFilePaths();

  await Promise.all(files.map(async (file) => {
    const contents = await fs.readFile(file, 'utf8')
    console.log(contents)
  }));
}
mesqueeb
  • 5,277
  • 5
  • 44
  • 77
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • 94
    Could you please explain why does `for ... of ...` work? – Demonbane Aug 15 '16 at 18:04
  • 232
    ok i know why... Using Babel will transform `async`/`await` to generator function and using `forEach` means that each iteration has an individual generator function, which has nothing to do with the others. so they will be executed independently and has no context of `next()` with others. Actually, a simple `for()` loop also works because the iterations are also in one single generator function. – Demonbane Aug 15 '16 at 19:21
  • 48
    @Demonbane: In short, because it was designed to work :-) `await` suspends the current *function* evaluation, including all control structures. Yes, it is quite similar to generators in that regard (which is why they are used to polyfill async/await). – Bergi Aug 15 '16 at 23:28
  • 2
    So `files.map(async (file) => ...` is equivalent to `files.map((file) => new Promise((rej, res) => { ...`? – arve0 Mar 29 '17 at 11:13
  • 8
    @arve0 Not really, an `async` function is quite different from a `Promise` executor callback, but yes the `map` callback returns a promise in both cases. – Bergi Mar 29 '17 at 16:25
  • 3
    This answer is the best one: ```await Promise.all(_.map(arr, async (val) => {...});``` solved my issue. Of course, each async callback returns a promise that I was not awaiting on. – Adi Sivasankaran Feb 23 '18 at 05:20
  • 3
    For those who don't know, [Promise.all](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all) returns a single promise that resolves when all the promises in the array are resolved. (basically waits for all promises to conclude) – Doug Mar 02 '18 at 12:47
  • I think the second paragraph should be "If you want to read the files in parallel, _you cannot use `for..of`_" ? Because you can read the files in parallel with the `forEach`, it's just that it will look very ugly because you cannot `await` it. – doubleOrt Mar 20 '18 at 13:17
  • 6
    @Taurus If you don't intend to await them, then `for…of` would work equally to `forEach`. No, I really mean that paragraph to emphasise that there is no place for `.forEach` in modern JS code. – Bergi Mar 20 '18 at 13:24
  • @Bergi Isn't that bad for performance ? If you need to make 3 requests, you can't make them all in parallel, so if each takes one second, your program will take 3 seconds to run. But if you use `map` or `forEach`, the requests will run in parallel. – doubleOrt Mar 20 '18 at 13:26
  • @Taurus I don't understand. What is bad for performance in comparison to what? – Bergi Mar 20 '18 at 13:28
  • 2
    `for..of` is bad for performance in comparison to `forEach`/`map`, because `for..of` will stop in each iteration, so if I make 3 requests, each request will have to wait for the preceding requests to complete. But with `forEach`/`map`, all the requests will be made in parallel. – doubleOrt Mar 20 '18 at 13:30
  • @Taurus No. `queries.forEach(q => asyncRequest(q));` does exactly the same as `for (const q of queries) asyncRequest(q);`. There is no difference in performance, and both will run the requests in parallel. Of course, in neither you can wait for anything - for how to do that, see my answer. – Bergi Mar 20 '18 at 13:33
  • @Bergi I am talking about this: https://gist.github.com/doubleOrt/598659adfefdd941f2a98512f0f7f078 – doubleOrt Mar 20 '18 at 13:45
  • @Taurus Yes, that's basically the two approaches from my answer. Neither of them uses `forEach`. Where's the problem? – Bergi Mar 20 '18 at 13:53
  • @Bergi I could have used `forEach` (would work the same as `map` regarding execution time). But as you can see from my previous comments, I was talking about either `forEach` or `map` versus `for..of`. I said: _for..of is bad for performance in comparison to forEach/map_. – doubleOrt Mar 20 '18 at 13:55
  • 1
    However, how is this true (from your answer): _If you want to read the files in parallel, you cannot use forEach indeed._ I think you can do that, same thing as your `map` example, except with `forEach` it will look ugly and unfit for an `async`/`await`. – doubleOrt Mar 20 '18 at 13:57
  • @Taurus But that's wrong: `for…of` is not bad for performance in comparison to `forEach` if used in the same unfit way without awaiting anything. And yes, "cannot be used" means "is unfit". – Bergi Mar 20 '18 at 14:05
  • @Bergi Are you suggesting a case where you don't `await` whatever request you make inside of a `for..of` ? If so, alright but you do agree that your second example is way more performant than your first example ? – doubleOrt Mar 20 '18 at 14:07
  • 1
    @Taurus Yes, that's what I wrote in my comment above. Of course it doesn't make any sense and is unfit to solve the OPs problem, regardless whether with `forEach` or `for…of`. And no, I would not compare the two (`for…of`+`await` vs `Promise.all`+`map`) in terms of "performance" at all - they are just different, in many other more important regards. Choosing sequential vs parallel executing is a decision that involves many other factors, and of course parallel typically finishes faster. – Bergi Mar 20 '18 at 14:14
  • @Bergi Of course there are cases where the `for..of` might be the only way to go (e.g if each request has a dependency on the previous request), but a beginner might always opt for the `for..of` solution because it is simpler. – doubleOrt Mar 20 '18 at 14:18
  • @Bergi However, I still don't understand how this is correct: _If you want to read the files in parallel, you cannot use forEach indeed._ – doubleOrt Mar 20 '18 at 14:18
  • @Taurus You cannot use it because you cannot await the result. Of course if you used it - like the OP did - then they would run in parallel, but `forEach` is absolutely **unfit** as you said yourself. – Bergi Mar 20 '18 at 14:23
  • You're a life saver. For the life of me I couldn't figure out why Object.entries({}).forEach wasn't returning the desired result. Switched it to: for (let [k, v] of Object.entries({})) {...} and it works great now. Thanks! – 010011100101 Mar 14 '21 at 08:02
  • `Void function return value is used ` – leonheess Aug 27 '21 at 18:47
  • @leonheess What's the problem? Where (what code position) are you getting that warning? What tool do you use? – Bergi Aug 27 '21 at 21:57
  • @Bergi my bad I used forEach instead of mal – leonheess Aug 28 '21 at 22:07
  • the best answer ;) Useful is to just think what the transpiler is doing with async / await and how the final code will look like – Griffi Sep 15 '21 at 21:58
  • @Griffi Why are you still using a transpiler for `async`/`await`? And what will it transpile to? – Bergi Sep 15 '21 at 22:26
  • There should be a `forEachAwait` operator that does just that! – Augustin Riedinger Dec 13 '21 at 15:22
  • @AugustinRiedinger It's called `for … of`! – Bergi Dec 13 '21 at 15:53
  • No, `for ... of` doesn't behave the same way: in one case, it takes a function (which can be `return`ed, curried, composed etc.) while in the other it is simply a `for` loop that must be `continue`d ... – Augustin Riedinger Dec 14 '21 at 08:45
  • @AugustinRiedinger You cannot return anything useful from a `forEach` callback anyway – Bergi Dec 14 '21 at 08:59
  • Well, those don't behave the same hence it could make sense that both versions exist. Just like `for ... of` and `forEach` also coexist. – Augustin Riedinger Dec 15 '21 at 12:22
  • You would need to return `contents`. – Adrian Bartholomew Jan 11 '22 at 16:57
  • @AdrianBartholomew OP only wants to log each file contents, and return nothing from the function. Of course if you want to create an array of file contents, you're free to `return` them from the `map` callback. – Bergi Jan 11 '22 at 17:10
  • 1
    Was struggling for 2 days using forEach where even after using async await, the loop never completed the iteration and the execution moved to next line. The for … of worked for me in completion of loop execution and then the execution moved to next line. – vinsinraw Jul 15 '22 at 16:01
  • To be clear for those who are confused at why `.forEach()` is bad, just stick a `console.log('callback')` at the end of your `.forEach()` async callback, and a `console.log('finished!')` after the `.forEach()` loop. You'll see that `finished!` gets logged out before the `callbacks`. Even if, for some reason, you actually wanted this sort of behavior, there's other ways to achieve it that doesn't make it look like you made a mistake in your code (like using an async IIFE inside the callback). – Scotty Jamison Jul 29 '22 at 14:34
  • This does not solve the use case for a forEach callback that takes both value and index and iterates over a huge sparse array. The of operator won't give you the index. – flodin Dec 17 '22 at 17:45
  • @flodin You can use `for (const [index, element]) of array.entries())` if you need an index. I would recommend to avoid sparse arrays, but if you have to, use `for … in` or `for (const element of array) if (element !== undefined)` – Bergi Dec 17 '22 at 17:53
  • @Bergi that will be crazy slow if your array is very sparse and has something set, say, at index 0x7fffffff. You need something that knows which indices are in use and only iterates over those. – flodin Dec 18 '22 at 13:48
  • 1
    @flodin That's just [what `forEach` does under the hood](https://tc39.es/ecma262/2022/multipage/indexed-collections.html#sec-array.prototype.foreach). If you need something faster, use `for … in`, as I suggested, or maybe `for (const index of Object.keys(sparse).map(Number))`. See also [here](https://stackoverflow.com/q/27598340/1048572) or [there](https://stackoverflow.com/q/10569106/1048572). If that doesn't solve your problem, you might want to [ask a new question](https://stackoverflow.com/questions/ask) about your concrete use case. – Bergi Dec 18 '22 at 15:45
  • @Bergi that would require every index to be first converted to a string and then back to an integer, which is also pretty slow. I have about one million entries spread across the billion possible indices of an array. Thanks for the information on the implementation of forEach, I had just assumed that they wouldn't have implemented it so poorly. I guess if I can't get around the need to iterate I'll have to switch to a Map instead. – flodin Dec 29 '22 at 09:53
  • It looks like using for...of loop is also shortest in terms of code length. So it has some advantages. – MAMY Sébastien Jun 30 '23 at 05:44
  • Is there a significant performance degradation using `map` instead of `forEach`? I don't care about the order, so I think that using a `for` loop with `awaits` in series would be slower. – Elijah Mock Jul 25 '23 at 20:55
  • 1
    @ElijahMock Both `map` and `forEach` start all tasks at the same time, allowing them to run concurrently. The difference is that `map` collects the results into an array and allows you to wait for them with `Promise.all`, whereas code with `forEach` just doesn't work. – Bergi Jul 25 '23 at 20:59
623

With ES2018, you are able to greatly simplify all of the above answers to:

async function printFiles () {
  const files = await getFilePaths()

  for await (const contents of files.map(file => fs.readFile(file, 'utf8'))) {
    console.log(contents)
  }
}

See spec: proposal-async-iteration

Simplified:

  for await (const results of array) {
    await longRunningTask()
  }
  console.log('I will wait')

2018-09-10: This answer has been getting a lot of attention recently, please see Axel Rauschmayer's blog post for further information about asynchronous iteration.

Steffan
  • 704
  • 1
  • 11
  • 25
Cisco
  • 20,972
  • 5
  • 38
  • 60
  • 14
    I don't think this answer address the initial question. `for-await-of` with a synchronous iterable (an array in our case) doesn’t cover the case of iterating concurrently an array using asynchronous operations in each iteration. If I'm not mistaken, using `for-await-of` with a synchronous iterable over non-promise values is the same as using a plain `for-of`. – Antonio Val Jan 09 '19 at 10:30
  • 3
    How we delegates `files` array to the `fs.readFile` here? It tooks from iterable? – Vadim Shvetsov Jan 17 '19 at 13:34
  • 1
    Using this solution each iteration would await for the previous, and in case of operation is making some long calculations or reading a long file it would block the executions of the next, as opposed to mapping all the functions to promises and waiting for them to complete. – Rafi Henig Sep 11 '19 at 01:07
  • 3
    This answer has the same issue as the OP: It accesses all files in parallel. The serialized printing of results merely hides it. – jib Feb 18 '21 at 13:52
  • The main issue with this answer is that in `for await`, the if using a sync iterator like an Array, the map should return a promise. In node fs is not async by default... using fsPromises would make this run in parallel properly. I think its good to bring attention to this superior, well supported syntax. – Ray Foss Apr 27 '21 at 05:17
  • 1
    Less characters does not mean it is simpler. This is mostly convoluted and unreadable. –  Aug 14 '21 at 16:40
  • 10
    This answer is wrong. `files.map()` returns an array of promises, **not an asynchronous iterator**, for which `for await` was made! [It will cause unhandled-rejection crashes](https://stackoverflow.com/a/59695815/1048572)! – Bergi Dec 13 '21 at 15:57
  • I think that in order to work, this should be `console.log(await contents);` because the library returns a promise and we need to wait for it to be fulfilled – Soldeplata Saketos Aug 18 '22 at 07:01
195

Instead of Promise.all in conjunction with Array.prototype.map (which does not guarantee the order in which the Promises are resolved), I use Array.prototype.reduce, starting with a resolved Promise:

async function printFiles () {
  const files = await getFilePaths();

  await files.reduce(async (promise, file) => {
    // This line will wait for the last async function to finish.
    // The first iteration uses an already resolved Promise
    // so, it will immediately continue.
    await promise;
    const contents = await fs.readFile(file, 'utf8');
    console.log(contents);
  }, Promise.resolve());
}
Timothy Zorn
  • 3,022
  • 2
  • 20
  • 18
  • 4
    This works perfectly, thank you so much. Could you explain what is happening here with `Promise.resolve()` and `await promise;`? – parrker9 Mar 28 '18 at 20:48
  • 2
    This is pretty cool. Am I right in thinking the files will be read in order and not all at once? – GollyJer Jun 09 '18 at 00:24
  • 7
    @parrker9 `Promise.resolve()` returns an already resolved `Promise` object, so that `reduce` has a `Promise` to start with. `await promise;` will wait for the last `Promise` in the chain to resolve. @GollyJer The files will be processed sequentially, one at a time. – Timothy Zorn Jun 17 '18 at 15:00
  • Very cool use of reduce, thanks for the comment! I'll just denote that, in contrast to some of the other methods mentioned in the comments, this one is synchronous, meaning that the files are read one after another and not in parallel (since the next iteration of reduce function relies on the previous iteration, it must be synchronous). – Shay Yzhakov May 30 '19 at 12:54
  • 3
    @Shay, You mean sequential, not synchronous. This is still asynchronous - if other things are scheduled, they will run in between the iterations here. – Timothy Zorn May 30 '19 at 16:51
  • Is there a way to let this apparently fast when you have an Express page that needs a list of resources to render...? –  Jan 30 '20 at 02:19
  • 6
    If you need the async processes to finish as quickly as possible and you don't care about them being completed sequentially, try one of the provided solutions with a good amount of upvotes which uses `Promise.all`. Example: `Promise.all(files.map(async (file) => { /* code */ }));` – Timothy Zorn Jan 31 '20 at 16:03
  • 1
    my favorite option, I'd love to see a native `Promise.chain()` like `all()` – Remi Mélisson Mar 18 '21 at 14:41
  • This works perfectly -- thanks, here the important step is await promise; that's what does the heavy load – Bigyan Devkota Jul 29 '21 at 21:44
  • This seems extremely clever, but it has some bizarre side effects. Everything in the reduce block before `await promise` will run asynchronously, and then each await and what's beyond it will execute synchronously. What is very unclear is why you don't pass a new promise back with each iteration. The initial promise is resolved before the first loop, so what is it awaiting? – joshstrike Jul 29 '22 at 22:56
  • This [async/await reduce blog post](https://www.stackfive.io/work/javascript/using-async-await-with-the-array-reduce-method) has the extra handy tip of putting a value e.g. an array inside the `Promise.resolve([])` and then using `const myArr = await promise` inside the reducer which allows you to also reduce a return value. – icc97 Sep 24 '22 at 12:28
  • This is ok as long as there are no errors. If there is an error thrown inside of the await reduce, it will crash nodejs. – Dev01 Dec 09 '22 at 03:08
  • Looks like hacky or very clever unreadable code. I think it's a really bad idea to put such a puzzle in your project – ixpl0 Jan 18 '23 at 09:52
96
files.forEach(async (file) => {
    const contents = await fs.readFile(file, 'utf8')
})

The issue is, the promise returned by the iteration function is ignored by forEach(). forEach does not wait to move to the next iteration after each async code execution is completed. All the fs.readFile functions will be invoked in the same round of the event loop, which means they are started in parallel, not in sequential, and the execution continues immediately after invoking forEach(), without waiting for all the fs.readFile operations to complete. Since forEach does not wait for each promise to resolve, the loop actually finishes iterating before promises are resolved. You are expecting that after forEach is completed, all the async code is already executed but that is not the case. You may end up trying to access values that are not available yet.

you can test the behaviour with this example code

const array = [1, 2, 3];

const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const delayedSquare = (num) => sleep(100).then(() => num * num);

const testForEach = (numbersArray) => {
  const store = [];
  // this code here treated as sync code
  numbersArray.forEach(async (num) => {
    const squaredNum = await delayedSquare(num);
    // this will console corrent squaredNum value
    // console.log(squaredNum) will log after console.log("store",store)
    console.log(squaredNum);
    store.push(squaredNum);
  });
  // you expect that store array is populated as [1,4,9] but it is not
  // this will return []
  console.log("store",store);
};
testForEach(array);
// Notice, when you test, first "store []" will be logged
// then squaredNum's inside forEach will log

the solution is using the for-of loop.

for (const file of files){
    const contents = await fs.readFile(file, 'utf8')
}
Yilmaz
  • 35,338
  • 10
  • 157
  • 202
63

Picture worth 1000 words - For Sequential Approach Only


Background : I was in similar situation last night. I used async function as foreach argument. The result was un-predictable. When I did testing for my code 3 times, it ran without issues 2 times and failed 1 time. (something weird)

Finally I got my head around & did some scratch pad testing.

Scenario 1 - How un-sequential it can get with async in foreach

enter image description here

const getPromise = (time) => { 
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`Promise resolved for ${time}s`)
    }, time)
  })
}

const main = async () => {
  const myPromiseArray = [getPromise(1000), getPromise(500), getPromise(3000)]
  console.log('Before For Each Loop')

  myPromiseArray.forEach(async (element, index) => {
    let result = await element;
    console.log(result);
  })

  console.log('After For Each Loop')
}

main();

Scenario 2 - Using for - of loop as @Bergi above suggested

enter image description here

const getPromise = (time) => { 
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`Promise resolved for ${time}s`)
    }, time)
  })
}

const main = async () => {
  const myPromiseArray = [getPromise(1000), getPromise(500), getPromise(3000)]
  console.log('Before For Each Loop')

  // AVOID USING THIS
  // myPromiseArray.forEach(async (element, index) => {
  //   let result = await element;
  //   console.log(result);
  // })

  // This works well
  for (const element of myPromiseArray) {
    let result = await element;
    console.log(result)
  }

  console.log('After For Each Loop')
}

main();

If you are little old school like me, you could simply use the classic for loop, that works too :)

const getPromise = (time) => { 
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`Promise resolved for ${time}s`)
    }, time)
  })
}

const main = async () => {
  const myPromiseArray = [getPromise(1000), getPromise(500), getPromise(3000)]
  console.log('Before For Each Loop')

  // AVOID USING THIS
  // myPromiseArray.forEach(async (element, index) => {
  //   let result = await element;
  //   console.log(result);
  // })

  // This works well too - the classic for loop :)
  for (let i = 0; i < myPromiseArray.length; i++) {
    const result = await myPromiseArray[i];
    console.log(result);
  }

  console.log('After For Each Loop')
}

main();

I hope this helps someone, good day, cheers!

krupesh Anadkat
  • 1,932
  • 1
  • 20
  • 31
  • 8
    If anyone wondering what vscode theme is that - its is github's official light theme. & If anyone hurt their eyes with so bright snapshot, my apologies – krupesh Anadkat May 19 '21 at 04:48
  • I suggest using the phrase 'Before/After Loop' would make it less confusing when it's not a 'For Each Loop'. – close Mar 02 '22 at 11:49
  • The brother is out here just writing code using Githubs official like an absolute heathen. I'm not even mad. To each their own. Nonetheless, I would cache the `length` to speed that for loop up and prevent recalculations between every iteration. – User_coder Apr 20 '22 at 00:03
  • 2
    Partially lost my sight on this one but totally worth it! – theFreedomBanana Dec 12 '22 at 22:49
61

The p-iteration module on npm implements the Array iteration methods so they can be used in a very straightforward way with async/await.

An example with your case:

const { forEach } = require('p-iteration');
const fs = require('fs-promise');

(async function printFiles () {
  const files = await getFilePaths();

  await forEach(files, async (file) => {
    const contents = await fs.readFile(file, 'utf8');
    console.log(contents);
  });
})();
Antonio Val
  • 3,200
  • 1
  • 14
  • 27
52

Here are some forEachAsync prototypes. Note you'll need to await them:

Array.prototype.forEachAsync = async function (fn) {
    for (let t of this) { await fn(t) }
}

Array.prototype.forEachAsyncParallel = async function (fn) {
    await Promise.all(this.map(fn));
}

Note while you may include this in your own code, you should not include this in libraries you distribute to others (to avoid polluting their globals).

mikemaccana
  • 110,530
  • 99
  • 389
  • 494
Matt
  • 2,096
  • 14
  • 20
  • 1
    usage : await myArray. forEachAsyncParallel( async (item) => { await myAsyncFunction(item) }) – Damien Romito Jun 02 '21 at 15:32
  • @Matt, isn't it a problem to await `fn` in case it wasn't asynchronous? what if the given input was a synchronous function? https://stackoverflow.com/a/53113299/18387350 – Normal Jun 22 '22 at 04:43
37

@Bergi has already gave the answer on how to handle this particular case properly. I'll not duplicate here.

I'd like to address the difference between using forEach and for loop when it comes to async and await

how forEach works

Let's look at how forEach works. According to ECMAScript Specification, MDN provides an implementation which can be used as a polyfill. I copy it and paste here with comments removal.

Array.prototype.forEach = function (callback, thisArg) {
  if (this == null) { throw new TypeError('Array.prototype.forEach called on null or undefined'); }
  var T, k;
  var O = Object(this);
  var len = O.length >>> 0;
  if (typeof callback !== "function") { throw new TypeError(callback + ' is not a function'); }
  if (arguments.length > 1) { T = thisArg; }
  k = 0;
  while (k < len) {
    var kValue;
    if (k in O) {
      kValue = O[k];
      callback.call(T, kValue, k, O); // pay attention to this line
    }
    k++;
  }
};

Let's back to your code, let's extract the callback as a function.

async function callback(file){
  const contents = await fs.readFile(file, 'utf8')
  console.log(contents)
}

So, basically callback returns a promise since it's declared with async. Inside forEach, callback is just called in a normal way, if the callback itself returns a promise, the javascript engine will not wait it to be resolved or rejected. Instead, it puts the promise in a job queue, and continues executing the loop.

How about await fs.readFile(file, 'utf8') inside the callback?

Basically, when your async callback gets the chance to be executed, the js engine will pause until fs.readFile(file, 'utf8') to be resolved or rejected, and resume execution of the async function after fulfillment. So the contents variable store the actual result from fs.readFile, not a promise. So, console.log(contents) logs out the file content not a Promise

Why for ... of works?

when we write a generic for of loop, we gain more control than forEach. Let's refactor printFiles.

async function printFiles () {
  const files = await getFilePaths() // Assume this works fine

  for (const file of files) {
    const contents = await fs.readFile(file, 'utf8')
    console.log(contents)
    // or await callback(file)
  }
}

When evaluate for loop, we have await promise inside the async function, the execution will pause until the await promise is settled. So, you can think of that the files are read one by one in a determined order.

Execute sequentially

Sometimes, we really need the the async functions to be executed in a sequential order. For example, I have a few new records stored in an array to be saved to database, and I want them to be saved in sequential order which means first record in array should be saved first, then second, until last one is saved.

Here is an example:

const records = [1, 2, 3, 4];

async function saveRecord(record) {
  return new Promise((resolved, rejected) => {
    setTimeout(()=> {
      resolved(`record ${record} saved`)
    }, Math.random() * 500)
  });
}

async function forEachSaveRecords(records) {
  records.forEach(async (record) => {
    const res = await saveRecord(record);
    console.log(res);
  })
}

async function forofSaveRecords(records) {
  for (const record of records) {
    const res = await saveRecord(record);
    console.log(res);
  }
}
(async () => {
  console.log("=== for of save records ===")
  await forofSaveRecords(records)
  
  console.log("=== forEach save records ===")
  await forEachSaveRecords(records)
})()

I use setTimeout to simulate the process of saving a record to database - it's asynchronous and cost a random time. Using forEach, the records are saved in an undetermined order, but using for..of, they are saved sequentially.

sam
  • 1,767
  • 12
  • 15
  • So in short: `foreach` doesn't handle callbacks in asynchronous way, therefore no waiting. – ado387 Jun 07 '22 at 17:50
  • I appreciate your effort. I am working on some puppeteer things, and I was wondering why my async, await is not working. your answer clarified my doubt. Like foreach, the issue is identical for the map, filter, etc. as well. – KAmit Aug 28 '22 at 15:52
29

This solution is also memory-optimized so you can run it on 10,000's of data items and requests. Some of the other solutions here will crash the server on large data sets.

In TypeScript:

export async function asyncForEach<T>(array: Array<T>, callback: (item: T, index: number) => Promise<void>) {
        for (let index = 0; index < array.length; index++) {
            await callback(array[index], index);
        }
    }

How to use?

await asyncForEach(receipts, async (eachItem) => {
    await ...
})
Oliver Dixon
  • 7,012
  • 5
  • 61
  • 95
  • I think it will be helpful if you can complete this example :) in the how to use section. For my case: await asyncForEach(configuration.groupNames, async (groupName) => { await AddUsersToGroup(configuration, groupName); }) – Ido Bleicher Oct 14 '21 at 08:10
  • Thanks, nice solution!! – JulienRioux Mar 12 '22 at 17:50
20

A simple drop-in solution for replacing a forEach() await loop that is not working is replacing forEach with map and adding Promise.all( to the beginning.

For example:

await y.forEach(async (x) => {

to

await Promise.all(y.map(async (x) => {

An extra ) is needed at the end.

yeah22
  • 420
  • 5
  • 12
  • 1
    Not quite the same. Promise.all will run all the promises _concurrently_. A for loop is meant to be sequential. – srmark Oct 07 '21 at 11:06
17

In addition to @Bergi’s answer, I’d like to offer a third alternative. It's very similar to @Bergi’s 2nd example, but instead of awaiting each readFile individually, you create an array of promises, each which you await at the end.

import fs from 'fs-promise';
async function printFiles () {
  const files = await getFilePaths();

  const promises = files.map((file) => fs.readFile(file, 'utf8'))

  const contents = await Promise.all(promises)

  contents.forEach(console.log);
}

Note that the function passed to .map() does not need to be async, since fs.readFile returns a Promise object anyway. Therefore promises is an array of Promise objects, which can be sent to Promise.all().

In @Bergi’s answer, the console may log file contents in the order they’re read. For example if a really small file finishes reading before a really large file, it will be logged first, even if the small file comes after the large file in the files array. However, in my method above, you are guaranteed the console will log the files in the same order as the provided array.

chharvey
  • 8,580
  • 9
  • 56
  • 95
12

it's pretty painless to pop a couple methods in a file that will handle asynchronous data in a serialized order and give a more conventional flavour to your code. For example:

module.exports = function () {
  var self = this;

  this.each = async (items, fn) => {
    if (items && items.length) {
      await Promise.all(
        items.map(async (item) => {
          await fn(item);
        }));
    }
  };

  this.reduce = async (items, fn, initialValue) => {
    await self.each(
      items, async (item) => {
        initialValue = await fn(initialValue, item);
      });
    return initialValue;
  };
};

now, assuming that's saved at './myAsync.js' you can do something similar to the below in an adjacent file:

...
/* your server setup here */
...
var MyAsync = require('./myAsync');
var Cat = require('./models/Cat');
var Doje = require('./models/Doje');
var example = async () => {
  var myAsync = new MyAsync();
  var doje = await Doje.findOne({ name: 'Doje', noises: [] }).save();
  var cleanParams = [];

  // FOR EACH EXAMPLE
  await myAsync.each(['bork', 'concern', 'heck'], 
    async (elem) => {
      if (elem !== 'heck') {
        await doje.update({ $push: { 'noises': elem }});
      }
    });

  var cat = await Cat.findOne({ name: 'Nyan' });

  // REDUCE EXAMPLE
  var friendsOfNyanCat = await myAsync.reduce(cat.friends,
    async (catArray, friendId) => {
      var friend = await Friend.findById(friendId);
      if (friend.name !== 'Long cat') {
        catArray.push(friend.name);
      }
    }, []);
  // Assuming Long Cat was a friend of Nyan Cat...
  assert(friendsOfNyanCat.length === (cat.friends.length - 1));
}
Jay Edwards
  • 950
  • 1
  • 12
  • 21
10

Bergi's solution works nicely when fs is promise based. You can use bluebird, fs-extra or fs-promise for this.

However, solution for node's native fs libary is as follows:

const result = await Promise.all(filePaths
    .map( async filePath => {
      const fileContents = await getAssetFromCache(filePath, async function() {

        // 1. Wrap with Promise    
        // 2. Return the result of the Promise
        return await new Promise((res, rej) => {
          fs.readFile(filePath, 'utf8', function(err, data) {
            if (data) {
              res(data);
            }
          });
        });
      });

      return fileContents;
    }));

Note: require('fs') compulsorily takes function as 3rd arguments, otherwise throws error:

TypeError [ERR_INVALID_CALLBACK]: Callback must be a function
master_dodo
  • 1,203
  • 3
  • 20
  • 31
10

It is not good to call an asynchronous method from a loop. This is because each loop iteration will be delayed until the entire asynchronous operation completes. That is not very performant. It also averts the advantages of parallelization benefits of async/await.

A better solution would be to create all promises at once, then get access to the results using Promise.all(). Otherwise, each successive operation will not start until the previous one has completed.

Consequently, the code may be refactored as follows;

const printFiles = async () => {
  const files = await getFilePaths();
  const results = [];
  files.forEach((file) => {
    results.push(fs.readFile(file, 'utf8'));
  });
  const contents = await Promise.all(results);
  console.log(contents);
}
Johnz
  • 270
  • 4
  • 13
  • 12
    It is also not good to open thousands of files at once to read them concurrently. One always has to do an assessment whether a sequential, parallel, or mixed approach is better. Sequential loops are not fundamentally bad, `await` actually makes them possible in the first place. Also they do not "aver the benefits" of asynchronous execution, as you can still run multiple such loops at once (e.g. two concurrent calls to `printFiles`). – Bergi Apr 02 '21 at 15:46
9

One important caveat is: The await + for .. of method and the forEach + async way actually have different effect.

Having await inside a real for loop will make sure all async calls are executed one by one. And the forEach + async way will fire off all promises at the same time, which is faster but sometimes overwhelmed(if you do some DB query or visit some web services with volume restrictions and do not want to fire 100,000 calls at a time).

You can also use reduce + promise(less elegant) if you do not use async/await and want to make sure files are read one after another.

files.reduce((lastPromise, file) => 
 lastPromise.then(() => 
   fs.readFile(file, 'utf8')
 ), Promise.resolve()
)

Or you can create a forEachAsync to help but basically use the same for loop underlying.

Array.prototype.forEachAsync = async function(cb){
    for(let x of this){
        await cb(x);
    }
}
LeOn - Han Li
  • 9,388
  • 1
  • 65
  • 59
  • Have a look at [How to define method in javascript on Array.prototype and Object.prototype so that it doesn't appear in for in loop](https://stackoverflow.com/q/13296340/1048572). Also you probably should use the same iteration as native `forEach` - accessing indices instead of relying on iterability - and pass the index to the callback. – Bergi Nov 16 '17 at 13:57
  • You can use `Array.prototype.reduce` in a way that uses an async function. I've shown an example in my answer: https://stackoverflow.com/a/49499491/2537258 – Timothy Zorn Mar 26 '18 at 19:54
8

Just adding to the original answer

  • The parallel reading syntax in the original answer is sometimes confusing and difficult to read, maybe we can write it in a different approach
async function printFiles() {
  const files = await getFilePaths();
  const fileReadPromises = [];

  const readAndLogFile = async filePath => {
    const contents = await fs.readFile(file, "utf8");
    console.log(contents);
    return contents;
  };

  files.forEach(file => {
    fileReadPromises.push(readAndLogFile(file));
  });

  await Promise.all(fileReadPromises);
}

  • For sequential operation, not just for...of, normal for loop will also work
async function printFiles() {
  const files = await getFilePaths();

  for (let i = 0; i < files.length; i++) {
    const file = files[i];
    const contents = await fs.readFile(file, "utf8");
    console.log(contents);
  }
}

gsaandy
  • 581
  • 4
  • 8
8

You can use Array.prototype.forEach, but async/await is not so compatible. This is because the promise returned from an async callback expects to be resolved, but Array.prototype.forEach does not resolve any promises from the execution of its callback. So then, you can use forEach, but you'll have to handle the promise resolution yourself.

Here is a way to read and print each file in series using Array.prototype.forEach

async function printFilesInSeries () {
  const files = await getFilePaths()

  let promiseChain = Promise.resolve()
  files.forEach((file) => {
    promiseChain = promiseChain.then(() => {
      fs.readFile(file, 'utf8').then((contents) => {
        console.log(contents)
      })
    })
  })
  await promiseChain
}

Here is a way (still using Array.prototype.forEach) to print the contents of files in parallel

async function printFilesInParallel () {
  const files = await getFilePaths()

  const promises = []
  files.forEach((file) => {
    promises.push(
      fs.readFile(file, 'utf8').then((contents) => {
        console.log(contents)
      })
    )
  })
  await Promise.all(promises)
}
richytong
  • 2,387
  • 1
  • 10
  • 21
7

Both the solutions above work, however, Antonio's does the job with less code, here is how it helped me resolve data from my database, from several different child refs and then pushing them all into an array and resolving it in a promise after all is done:

Promise.all(PacksList.map((pack)=>{
    return fireBaseRef.child(pack.folderPath).once('value',(snap)=>{
        snap.forEach( childSnap => {
            const file = childSnap.val()
            file.id = childSnap.key;
            allItems.push( file )
        })
    })
})).then(()=>store.dispatch( actions.allMockupItems(allItems)))
Hooman Askari
  • 1,486
  • 16
  • 30
7

Like @Bergi's response, but with one difference.

Promise.all rejects all promises if one gets rejected.

So, use a recursion.

const readFilesQueue = async (files, index = 0) {
    const contents = await fs.readFile(files[index], 'utf8')
    console.log(contents)

    return files.length <= index
        ? readFilesQueue(files, ++index)
        : files

}

const printFiles async = () => {
    const files = await getFilePaths();
    const printContents = await readFilesQueue(files)

    return printContents
}

printFiles()

PS

readFilesQueue is outside of printFiles cause the side effect* introduced by console.log, it's better to mock, test, and or spy so, it's not cool to have a function that returns the content(sidenote).

Therefore, the code can simply be designed by that: three separated functions that are "pure"** and introduce no side effects, process the entire list and can easily be modified to handle failed cases.

const files = await getFilesPath()

const printFile = async (file) => {
    const content = await fs.readFile(file, 'utf8')
    console.log(content)
}

const readFiles = async = (files, index = 0) => {
    await printFile(files[index])

    return files.lengh <= index
        ? readFiles(files, ++index)
        : files
}

readFiles(files)

Future edit/current state

Node supports top-level await (this doesn't have a plugin yet, won't have and can be enabled via harmony flags), it's cool but doesn't solve one problem (strategically I work only on LTS versions). How to get the files?

Using composition. Given the code, causes to me a sensation that this is inside a module, so, should have a function to do it. If not, you should use an IIFE to wrap the role code into an async function creating simple module that's do all for you, or you can go with the right way, there is, composition.

// more complex version with IIFE to a single module
(async (files) => readFiles(await files())(getFilesPath)

Note that the name of variable changes due to semantics. You pass a functor (a function that can be invoked by another function) and recieves a pointer on memory that contains the initial block of logic of the application.

But, if's not a module and you need to export the logic?

Wrap the functions in a async function.

export const readFilesQueue = async () => {
    // ... to code goes here
}

Or change the names of variables, whatever...


* by side effect menans any colacteral effect of application that can change the statate/behaviour or introuce bugs in the application, like IO.

** by "pure", it's in apostrophe since the functions it's not pure and the code can be converged to a pure version, when there's no console output, only data manipulations.

Aside this, to be pure, you'll need to work with monads that handles the side effect, that are error prone, and treats that error separately of the application.

lukaswilkeer
  • 305
  • 4
  • 13
6

Today I came across multiple solutions for this. Running the async await functions in the forEach Loop. By building the wrapper around we can make this happen.

More detailed explanation on how it works internally, for the native forEach and why it is not able to make a async function call and other details on the various methods are provided in link here

The multiple ways through which it can be done and they are as follows,

Method 1 : Using the wrapper.

await (()=>{
     return new Promise((resolve,reject)=>{
       items.forEach(async (item,index)=>{
           try{
               await someAPICall();
           } catch(e) {
              console.log(e)
           }
           count++;
           if(index === items.length-1){
             resolve('Done')
           }
         });
     });
    })();

Method 2: Using the same as a generic function of Array.prototype

Array.prototype.forEachAsync.js

if(!Array.prototype.forEachAsync) {
    Array.prototype.forEachAsync = function (fn){
      return new Promise((resolve,reject)=>{
        this.forEach(async(item,index,array)=>{
            await fn(item,index,array);
            if(index === array.length-1){
                resolve('done');
            }
        })
      });
    };
  }

Usage :

require('./Array.prototype.forEachAsync');

let count = 0;

let hello = async (items) => {

// Method 1 - Using the Array.prototype.forEach 

    await items.forEachAsync(async () => {
         try{
               await someAPICall();
           } catch(e) {
              console.log(e)
           }
        count++;
    });

    console.log("count = " + count);
}

someAPICall = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("done") // or reject('error')
        }, 100);
    })
}

hello(['', '', '', '']); // hello([]) empty array is also be handled by default

Method 3 :

Using Promise.all

  await Promise.all(items.map(async (item) => {
        await someAPICall();
        count++;
    }));

    console.log("count = " + count);

Method 4 : Traditional for loop or modern for loop

// Method 4 - using for loop directly

// 1. Using the modern for(.. in..) loop
   for(item in items){

        await someAPICall();
        count++;
    }

//2. Using the traditional for loop 

    for(let i=0;i<items.length;i++){

        await someAPICall();
        count++;
    }


    console.log("count = " + count);
PranavKAndro
  • 921
  • 7
  • 9
  • Your methods 1 and 2 are simply incorrect implementations where `Promise.all` should have been used - they do not take any of the many edge cases into account. – Bergi Nov 24 '19 at 21:48
  • @Bergi: Thanks for the valid comments, Would you please explain me why method 1 and 2 are incorrect. It also serves the purpose. This works very well. This is to say that all these methods are possible, based on the situation one can decide on choosing one. I have the running example for the same. – PranavKAndro Nov 25 '19 at 14:15
  • It fails on empty arrays, it doesn't have any error handling, and probably more problems. Don't reinvent the wheel. Just use `Promise.all`. – Bergi Nov 25 '19 at 15:25
  • In certain conditions where its not possible it will be helpful. Also error handling is done by forEach api by default so no issues. Its taken care ! – PranavKAndro Nov 26 '19 at 05:57
  • No, there are no conditions where `Promise.all` is not possible but `async`/`await` is. And no, `forEach` absolutely doesn't handle any promise errors. – Bergi Nov 26 '19 at 13:22
  • Cone on Bergi, its not the matter of which is best, but the various available solutions. You mentioned error handling as empty array, empty array error handling is taken care by forEach internally. Lets drop this conversation here. Again its all the possible ways you can achieve – PranavKAndro Nov 26 '19 at 17:22
  • It's not about "good" vs "better" from a pool of valid solutions, but that your code is completely broken. Have you tried what happens when you pass in an empty array? It hangs forever. Have you tried what happens when `someAPICall()` rejects? You get an unhandled promise rejection and it hangs forever. – Bergi Nov 26 '19 at 17:26
  • @Bergi: It is usual that if any developer use the async await to wait for the promise, then it should be surrounded by the try catch. someAPICall() is not the wrapper that I am providing, It is the developer function that developer write inside the callback that I am providing as part of wrapper, developer should surround that with try catch. If you want i can update the answer, Empty array is handled, you run and let me know. It works perfect. – PranavKAndro Nov 27 '19 at 05:39
  • "*developer should surround that with try catch*" - and yet you didn't. If you think one always would need to do this, yes please at least do it yourself. But no, the usual expectation is that the promise returned by `forEachAsync(…)` should reject. And no, your code does not handle empty arrays, did you try it? `await [].forEachAsync(() => {}); console.log("never happens");` – Bergi Nov 27 '19 at 09:25
5

Currently the Array.forEach prototype property doesn't support async operations, but we can create our own poly-fill to meet our needs.

// Example of asyncForEach Array poly-fill for NodeJs
// file: asyncForEach.js
// Define asynForEach function 
async function asyncForEach(iteratorFunction){
  let indexer = 0
  for(let data of this){
    await iteratorFunction(data, indexer)
    indexer++
  }
}
// Append it as an Array prototype property
Array.prototype.asyncForEach = asyncForEach
module.exports = {Array}

And that's it! You now have an async forEach method available on any arrays that are defined after these to operations.

Let's test it...

// Nodejs style
// file: someOtherFile.js

const readline = require('readline')
Array = require('./asyncForEach').Array
const log = console.log

// Create a stream interface
function createReader(options={prompt: '>'}){
  return readline.createInterface({
    input: process.stdin
    ,output: process.stdout
    ,prompt: options.prompt !== undefined ? options.prompt : '>'
  })
}
// Create a cli stream reader
async function getUserIn(question, options={prompt:'>'}){
  log(question)
  let reader = createReader(options)
  return new Promise((res)=>{
    reader.on('line', (answer)=>{
      process.stdout.cursorTo(0, 0)
      process.stdout.clearScreenDown()
      reader.close()
      res(answer)
    })
  })
}

let questions = [
  `What's your name`
  ,`What's your favorite programming language`
  ,`What's your favorite async function`
]
let responses = {}

async function getResponses(){
// Notice we have to prepend await before calling the async Array function
// in order for it to function as expected
  await questions.asyncForEach(async function(question, index){
    let answer = await getUserIn(question)
    responses[question] = answer
  })
}

async function main(){
  await getResponses()
  log(responses)
}
main()
// Should prompt user for an answer to each question and then 
// log each question and answer as an object to the terminal

We could do the same for some of the other array functions like map...

async function asyncMap(iteratorFunction){
  let newMap = []
  let indexer = 0
  for(let data of this){
    newMap[indexer] = await iteratorFunction(data, indexer, this)
    indexer++
  }
  return newMap
}

Array.prototype.asyncMap = asyncMap

... and so on :)

Some things to note:

  • Your iteratorFunction must be an async function or promise
  • Any arrays created before Array.prototype.<yourAsyncFunc> = <yourAsyncFunc> will not have this feature available
mikemaccana
  • 110,530
  • 99
  • 389
  • 494
Beau
  • 71
  • 1
  • 4
5

To see how that can go wrong, print console.log at the end of the method.

Things that can go wrong in general:

  • Arbitrary order.
  • printFiles can finish running before printing files.
  • Poor performance.

These are not always wrong but frequently are in standard use cases.

Generally, using forEach will result in all but the last. It'll call each function without awaiting for the function meaning it tells all of the functions to start then finishes without waiting for the functions to finish.

import fs from 'fs-promise'

async function printFiles () {
  const files = (await getFilePaths()).map(file => fs.readFile(file, 'utf8'))

  for(const file of files)
    console.log(await file)
}

printFiles()

This is an example in native JS that will preserve order, prevent the function from returning prematurely and in theory retain optimal performance.

This will:

  • Initiate all of the file reads to happen in parallel.
  • Preserve the order via the use of map to map file names to promises to wait for.
  • Wait for each promise in the order defined by the array.

With this solution the first file will be shown as soon as it is available without having to wait for the others to be available first.

It will also be loading all files at the same time rather than having to wait for the first to finish before the second file read can be started.

The only draw back of this and the original version is that if multiple reads are started at once then it's more difficult to handle errors on account of having more errors that can happen at a time.

With versions that read a file at a time then then will stop on a failure without wasting time trying to read any more files. Even with an elaborate cancellation system it can be hard to avoid it failing on the first file but reading most of the other files already as well.

Performance is not always predictable. While many systems will be faster with parallel file reads some will prefer sequential. Some are dynamic and may shift under load, optimisations that offer latency do not always yield good throughput under heavy contention.

There is also no error handling in that example. If something requires them to either all be successfully shown or not at all it won't do that.

In depth experimentation is recommended with console.log at each stage and fake file read solutions (random delay instead). Although many solutions appear to do the same in simple cases all have subtle differences that take some extra scrutiny to squeeze out.

Use this mock to help tell the difference between solutions:

(async () => {
  const start = +new Date();
  const mock = () => {
    return {
      fs: {readFile: file => new Promise((resolve, reject) => {
        // Instead of this just make three files and try each timing arrangement.
        // IE, all same, [100, 200, 300], [300, 200, 100], [100, 300, 200], etc.
        const time = Math.round(100 + Math.random() * 4900);
        console.log(`Read of ${file} started at ${new Date() - start} and will take ${time}ms.`)
        setTimeout(() => {
          // Bonus material here if random reject instead.
          console.log(`Read of ${file} finished, resolving promise at ${new Date() - start}.`);
          resolve(file);
        }, time);
      })},
      console: {log: file => console.log(`Console Log of ${file} finished at ${new Date() - start}.`)},
      getFilePaths: () => ['A', 'B', 'C', 'D', 'E']
    };
  };

  const printFiles = (({fs, console, getFilePaths}) => {
    return async function() {
      const files = (await getFilePaths()).map(file => fs.readFile(file, 'utf8'));

      for(const file of files)
        console.log(await file);
    };
  })(mock());

  console.log(`Running at ${new Date() - start}`);
  await printFiles();
  console.log(`Finished running at ${new Date() - start}`);
})();

jgmjgm
  • 4,240
  • 1
  • 25
  • 18
5

The OP's original question

Are there any issues with using async/await in a forEach loop? ...

was covered to an extent in @Bergi's selected answer, which showed how to process in serial and in parallel. However there are other issues noted with parallelism -

  1. Order -- @chharvey notes that -

For example if a really small file finishes reading before a really large file, it will be logged first, even if the small file comes after the large file in the files array.

  1. Possibly opening too many files at once -- A comment by Bergi under another answer

It is also not good to open thousands of files at once to read them concurrently. One always has to do an assessment whether a sequential, parallel, or mixed approach is better.

So let's address these issues showing actual code that is brief and concise, and does not use third party libraries. Something easy to cut, paste, and modify.

Reading in parallel (all at once), printing in serial (as early as possible per file).

The easiest improvement is to perform full parallelism as in @Bergi's answer, but making a small change so that each file is printed as soon as possible while preserving order.

async function printFiles2() {
  const readProms = (await getFilePaths()).map((file) =>
    fs.readFile(file, "utf8")
  );
  await Promise.all([
    await Promise.all(readProms),                      // branch 1
    (async () => {                                     // branch 2
      for (const p of readProms) console.log(await p);
    })(),
  ]);
}

Above, two separate branches are run concurrently.

  • branch 1: Reading in parallel, all at once,
  • branch 2: Reading in serial to force order, but waiting no longer than necessary

That was easy.

Reading in parallel with a concurrency limit, printing in serial (as early as possible per file).

A "concurrency limit" means that no more than N files will ever being read at the same time.
Like a store that only allows in so many customers at a time (at least during COVID).

First a helper function is introduced -

function bootablePromise(kickMe: () => Promise<any>) {
  let resolve: (value: unknown) => void = () => {};
  const promise = new Promise((res) => { resolve = res; });
  const boot = () => { resolve(kickMe()); };
  return { promise, boot };
}

The function bootablePromise(kickMe:() => Promise<any>) takes a function kickMe as an argument to start a task (in our case readFile) but is not started immediately.

bootablePromise returns a couple of properties

  • promise of type Promise
  • boot of type function ()=>void

promise has two stages in life

  1. Being a promise to start a task
  2. Being a promise complete a task it has already started.

promise transitions from the first to the second state when boot() is called.

bootablePromise is used in printFiles --

async function printFiles4() {
  const files = await getFilePaths();
  const boots: (() => void)[] = [];
  const set: Set<Promise<{ pidx: number }>> = new Set<Promise<any>>();
  const bootableProms = files.map((file,pidx) => {
    const { promise, boot } = bootablePromise(() => fs.readFile(file, "utf8"));
    boots.push(boot);
    set.add(promise.then(() => ({ pidx })));
    return promise;
  });
  const concurLimit = 2;
  await Promise.all([
    (async () => {                                       // branch 1
      let idx = 0;
      boots.slice(0, concurLimit).forEach((b) => { b(); idx++; });
      while (idx<boots.length) {
        const { pidx } = await Promise.race([...set]);
        set.delete([...set][pidx]);
        boots[idx++]();
      }
    })(),
    (async () => {                                       // branch 2
      for (const p of bootableProms) console.log(await p);
    })(),
  ]);
}

As before there are two branches

  • branch 1: For running and handling concurrency.
  • branch 2: For printing

The difference now is the no more than concurLimit Promises are allowed to run concurrently.

The important variables are

  • boots: The array of functions to call to force its corresponding Promise to transition. It is used only in branch 1.
  • set: There are Promises in a random access container so that they can be easily removed once fulfilled. This container is used only in branch 1.
  • bootableProms: These are the same Promises as initially in set, but it is an array not a set, and the array is never changed. It is used only in branch 2.

Running with a mock fs.readFile that takes times as follows (filename vs. time in ms).

const timeTable = {
  "1": 600,
  "2": 500,
  "3": 400,
  "4": 300,
  "5": 200,
  "6": 100,
};

test run times such as this are seen, showing the concurrency is working --

[1]0--0.601
[2]0--0.502
[3]0.503--0.904
[4]0.608--0.908
[5]0.905--1.105
[6]0.905--1.005

Available as executable in the typescript playground sandbox

LinuxDisciple
  • 2,289
  • 16
  • 19
Craig Hicks
  • 2,199
  • 20
  • 35
4

Using Task, futurize, and a traversable List, you can simply do

async function printFiles() {
  const files = await getFiles();

  List(files).traverse( Task.of, f => readFile( f, 'utf-8'))
    .fork( console.error, console.log)
}

Here is how you'd set this up

import fs from 'fs';
import { futurize } from 'futurize';
import Task from 'data.task';
import { List } from 'immutable-ext';

const future = futurizeP(Task)
const readFile = future(fs.readFile)

Another way to have structured the desired code would be

const printFiles = files => 
  List(files).traverse( Task.of, fn => readFile( fn, 'utf-8'))
    .fork( console.error, console.log)

Or perhaps even more functionally oriented

// 90% of encodings are utf-8, making that use case super easy is prudent

// handy-library.js
export const readFile = f =>
  future(fs.readFile)( f, 'utf-8' )

export const arrayToTaskList = list => taskFn => 
  List(files).traverse( Task.of, taskFn ) 

export const readFiles = files =>
  arrayToTaskList( files, readFile )

export const printFiles = files => 
  readFiles(files).fork( console.error, console.log)

Then from the parent function

async function main() {
  /* awesome code with side-effects before */
  printFiles( await getFiles() );
  /* awesome code with side-effects after */
}

If you really wanted more flexibility in encoding, you could just do this (for fun, I'm using the proposed Pipe Forward operator )

import { curry, flip } from 'ramda'

export const readFile = fs.readFile 
  |> future,
  |> curry,
  |> flip

export const readFileUtf8 = readFile('utf-8')

PS - I didn't try this code on the console, might have some typos... "straight freestyle, off the top of the dome!" as the 90s kids would say. :-p

Babakness
  • 2,954
  • 3
  • 17
  • 28
2

As other answers have mentioned, you're probably wanting it to be executed in sequence rather in parallel. Ie. run for first file, wait until it's done, then once it's done run for second file. That's not what will happen.

I think it's important to address why this doesn't happen.

Think about how forEach works. I can't find the source, but I presume it works something like this:

const forEach = (arr, cb) => {
  for (let i = 0; i < arr.length; i++) {
    cb(arr[i]);
  }
};

Now think about what happens when you do something like this:

forEach(files, async logFile(file) {
  const contents = await fs.readFile(file, 'utf8');
  console.log(contents);
});

Inside forEach's for loop we're calling cb(arr[i]), which ends up being logFile(file). The logFile function has an await inside it, so maybe the for loop will wait for this await before proceeding to i++?

No, it won't. Confusingly, that's not how await works. From the docs:

An await splits execution flow, allowing the caller of the async function to resume execution. After the await defers the continuation of the async function, execution of subsequent statements ensues. If this await is the last expression executed by its function execution continues by returning to the function's caller a pending Promise for completion of the await's function and resuming execution of that caller.

So if you have the following, the numbers won't be logged before "b":

const delay = (ms) => {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
};

const logNumbers = async () => {
  console.log(1);
  await delay(2000);
  console.log(2);
  await delay(2000);
  console.log(3);
};

const main = () => {
  console.log("a");
  logNumbers();
  console.log("b");
};

main();

Circling back to forEach, forEach is like main and logFile is like logNumbers. main won't stop just because logNumbers does some awaiting, and forEach won't stop just because logFile does some awaiting.

Adam Zerner
  • 17,797
  • 15
  • 90
  • 156
2

Here is a great example for using async in forEach loop.

Write your own asyncForEach

async function asyncForEach(array, callback) {  
    for (let index = 0; index < array.length; index++) {
        await callback(array[index], index, array)
    }
}

You can use it like this

await asyncForEach(array, async function(item,index,array){
     //await here
   }
)
Jellow
  • 93
  • 6
1

Similar to Antonio Val's p-iteration, an alternative npm module is async-af:

const AsyncAF = require('async-af');
const fs = require('fs-promise');

function printFiles() {
  // since AsyncAF accepts promises or non-promises, there's no need to await here
  const files = getFilePaths();

  AsyncAF(files).forEach(async file => {
    const contents = await fs.readFile(file, 'utf8');
    console.log(contents);
  });
}

printFiles();

Alternatively, async-af has a static method (log/logAF) that logs the results of promises:

const AsyncAF = require('async-af');
const fs = require('fs-promise');

function printFiles() {
  const files = getFilePaths();

  AsyncAF(files).forEach(file => {
    AsyncAF.log(fs.readFile(file, 'utf8'));
  });
}

printFiles();

However, the main advantage of the library is that you can chain asynchronous methods to do something like:

const aaf = require('async-af');
const fs = require('fs-promise');

const printFiles = () => aaf(getFilePaths())
  .map(file => fs.readFile(file, 'utf8'))
  .forEach(file => aaf.log(file));

printFiles();

async-af

Scott Rudiger
  • 1,224
  • 12
  • 16
0

If you'd like to iterate over all elements concurrently:

async function asyncForEach(arr, fn) {
  await Promise.all(arr.map(fn));
}

If you'd like to iterate over all elements non-concurrently (e.g. when your mapping function has side effects or running mapper over all array elements at once would be too resource costly):

Option A: Promises

function asyncForEachStrict(arr, fn) {
  return new Promise((resolve) => {
    arr.reduce(
      (promise, cur, idx) => promise
        .then(() => fn(cur, idx, arr)),
      Promise.resolve(),
    ).then(() => resolve());
  });
}

Option B: async/await

async function asyncForEachStrict(arr, fn) {
  for (let idx = 0; idx < arr.length; idx += 1) {
    const cur = arr[idx];

    await fn(cur, idx, arr);
  }
}
Wojciech Maj
  • 982
  • 6
  • 21
  • Your option a involves the [`Promise` constructor antipattern](https://stackoverflow.com/q/23803743/1048572?What-is-the-promise-construction-antipattern-and-how-to-avoid-it). – Bergi Nov 19 '20 at 15:02
0

This does not use async/await as the OP requested and only works if you are in the back-end with NodeJS. Although it still may be helpful for some people, because the example given by OP is to read file contents, and normally you do file reading in the backend.

Fully asynchronous and non-blocking:

const fs = require("fs")
const async = require("async")

const obj = {dev: "/dev.json", test: "/test.json", prod: "/prod.json"}
const configs = {}

async.forEachOf(obj, (value, key, callback) => {
    fs.readFile(__dirname + value, "utf8", (err, data) => {
        if (err) return callback(err)
        try {
            configs[key] = JSON.parse(data);
        } catch (e) {
            return callback(e)
        }
        callback()
    });
}, err => {
    if (err) console.error(err.message)
    // configs is now a map of JSON data
    doSomethingWith(configs)
})
João Pimentel Ferreira
  • 14,289
  • 10
  • 80
  • 109
  • OP never requested not to use `async`/`await`. They state "*I'm trying to loop through an array of files and `await` on the contents of each file.*" – Bergi Jan 10 '22 at 00:12
  • Also, why do you say `require("async").forEach` only works in nodejs? – Bergi Jan 10 '22 at 00:14
  • @Bergi I _explicitly_ said the OP didn't request exactly that and it just works with NodeJS. Although it still may be helpful for some people, *because the example given by OP is to read file contents*, and normally you do file reading in the backend. – João Pimentel Ferreira Jan 10 '22 at 11:17
  • Oh, I misinterpreted that phrase as "does (not use async/await) as the OP requested" instead of "does not (use async/await as the OP requested)" – Bergi Jan 10 '22 at 11:23
-1

For TypeScript users, a Promise.all(array.map(iterator)) wrapper with working types

  • Using Promise.all(array.map(iterator)) has correct types since the TypeScript's stdlib support already handles generics.
  • However copy pasting Promise.all(array.map(iterator)) every time you need an async map is obviously suboptimal, and Promise.all(array.map(iterator)) doesn't convey the intention of the code very well - so most developers would wrap this into an asyncMap() wrapper function. However doing this requires use of generics to ensure that values set with const value = await asyncMap() have the correct type.
export const asyncMap = async <ArrayItemType, IteratorReturnType>(
  array: Array<ArrayItemType>,
  iterator: (
    value: ArrayItemType,
    index?: number
  ) => Promise<IteratorReturnType>
): Promise<Array<IteratorReturnType>> => {
  return Promise.all(array.map(iterator));
};

And a quick test:

it(`runs 3 items in parallel and returns results`, async () => {
  const result = await asyncMap([1, 2, 3], async (item: number) => {
    await sleep(item * 100);
    return `Finished ${item}`;
  });
  expect(result.length).toEqual(3);
  // Each item takes 100, 200 and 300ms
  // So restricting this test to 300ms plus some leeway
}, 320);

sleep() is just:

const sleep = async (timeInMs: number): Promise<void> => {
  return new Promise((resolve) => setTimeout(resolve, timeInMs));
};
mikemaccana
  • 110,530
  • 99
  • 389
  • 494
  • If anyone has feedback on this answer please let me know - I generally believe most programmers wouldn't want to copy paste `Promise.all(array.map(iterator))` rather than just have a single function, and sadly wrapping `Promise.all(array.map(iterator))` without generics won't have the correct types. The answer also isn't a duplicate, and should be helpful to anyone using async/await and TS, so if there's something I can improve (which there seems to be from the voting so far) please tell me. – mikemaccana Dec 30 '22 at 21:42
-2

I would use the well-tested (millions of downloads per week) pify and async modules. If you are unfamiliar with the async module, I highly recommend you check out its docs. I've seen multiple devs waste time recreating its methods, or worse, making difficult-to-maintain async code when higher-order async methods would simplify code.

const async = require('async')
const fs = require('fs-promise')
const pify = require('pify')

async function getFilePaths() {
    return Promise.resolve([
        './package.json',
        './package-lock.json',
    ]);
}

async function printFiles () {
  const files = await getFilePaths()

  await pify(async.eachSeries)(files, async (file) => {  // <-- run in series
  // await pify(async.each)(files, async (file) => {  // <-- run in parallel
    const contents = await fs.readFile(file, 'utf8')
    console.log(contents)
  })
  console.log('HAMBONE')
}

printFiles().then(() => {
    console.log('HAMBUNNY')
})
// ORDER OF LOGS:
// package.json contents
// package-lock.json contents
// HAMBONE
// HAMBUNNY
```
Zachary Ryan Smith
  • 2,688
  • 1
  • 20
  • 30
  • 1
    This is a step in the wrong direction. Here's a mapping guide I created to help get folks stuck in callback hell into the modern JS era: https://github.com/jmjpro/async-package-to-async-await/blob/master/README.md. – jbustamovej Feb 20 '18 at 06:24
  • as you [can see here](https://github.com/ZacharyRSmith/javascript-async-await-promise-katas/pull/6), I am interested in and open to using async/await instead of the async lib. Right now, I think that each has a time and place. I'm not convinced that the async lib == "callback hell" and async/await == "the modern JS era". imo, when async lib > async/await: 1. complex flow (eg, queue, cargo, even auto when things get complicated) 2. concurrency 3. supporting arrays/objects/iterables 4. err handling – Zachary Ryan Smith Feb 21 '18 at 01:54
-2

In 2022 I would still advise using external libraries to handle all this async flow. I've created the module alot for similar things.

Your example would be:

import fs from 'fs-promise'
import alot from 'alot'

async function printFiles () {
    const files = await getFilePaths() // Assume this works fine

    await alot(files)
        .forEachAsync(async file => {
            let content = await fs.readFile(file, 'utf8');
            console.log(content);
        })
        .toArrayAsync({ threads: 4 });
    }
}
printFiles()

For simple examples surely the async for..of would do the job, but as soon the task is more complicated you have to use some utility for this.

Alot has dozens of other methods that you can chain, like mapAsync, filterAsync, groupAsync, etc.

As an example:

  • Load JSON files with products meta
  • Extract ProductID
  • Load products from the server
  • Filter those with a price > 100$
  • Order by price ascending
  • Take top 50

import fs from 'fs-promise'
import alot from 'alot'
import axios from 'axios'
import { File } from 'atma-io'

let paths = await getFilePaths();
let products = await alot(paths)
    .mapAsync(async path => await File.readAsync<IProductMeta>(path))
    .mapAsync(async meta => await axios.get(`${server}/api/product/${meta.productId}`))
    .mapAsync(resp => resp.data)
    .filterAsync(product => product.price > 100)
    .sortBy(product => product.price, 'asc')
    .takeAsync(50)
    .toArrayAsync({ threads: 5, errors: 'include' });
Heretic Monkey
  • 11,687
  • 7
  • 53
  • 122
tenbits
  • 7,568
  • 5
  • 34
  • 53
  • What is `threads: 4`? JS doesn't have threads – Bergi Apr 06 '22 at 13:52
  • @Bergi But the underlying layer has. All this `async\await` story means the event-loop waits until it gets the result back. By defining `threads` we set how many tasks we start parallel, other will wait until at least on task (fs, network, worker, etc.) is ready. – tenbits Apr 06 '22 at 16:58
-3

You can use the async.forEach loop from the async package:

async.forEach(dataToLoop(array), async(data, cb) => {
                variable = await MongoQuery;
            }, function(err) {
                console.log(err);  
              })
            })
            .catch((err)=>{
              console.log(err);
            })
jatinS
  • 566
  • 6
  • 6