3

I'm trying to put a delay between each iteration in a loop using async await. I've got a helper sleep function:

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

And this is correctly waiting between each loop:

for (let i = 0; i < 5; i++) {
    console.log('waiting')
    await sleep(1000)
}

However, this is not waiting between each loop:

[0, 1, 2, 3, 4].forEach(async () => {
    console.log('waiting')
    await sleep(1000)
});

How can I modify the forEach code block to behave as the regular for loop block with delays between each iteration of the for loop?

Jeremy
  • 1
  • 85
  • 340
  • 366
Andrew
  • 497
  • 6
  • 21
  • 1
    Since you're using a modern version of ECMAScript (with `async`/`await`), you probably don't want to use `.forEach`. It was from before the language had proper iterators, which you can now use with `for (let/const value of array)` (instead of a for-in loop as you have, which are also more-or-less obsolete). – Jeremy Apr 01 '19 at 23:06
  • To make this work, `forEach` itself would have to `await` the promise returned by the callback. But `.forEach` completely ignores the return value. – Felix Kling Apr 01 '19 at 23:26

3 Answers3

1

You could theoretically build up a promise chain, thats slightly more beautiful with reduce, but the same pattern can also be done with forEach:

[0, 1, 2, 3, 4].reduce(async (previous) => {
  await previous;
  console.log('waiting')
  await sleep(1000)
});

But ... why not just use a for loop?

Jeremy
  • 1
  • 85
  • 340
  • 366
Jonas Wilms
  • 132,000
  • 20
  • 149
  • 151
  • Actually I was looking to do what you suggested with reduce...I don't know why I used forEach in my example And no real good reason to use reduce over a for loop other than it being slightly cleaner for my purpose so I was just wondering if there was an easy way to achieve this that I was overlooking – Andrew Apr 01 '19 at 23:09
  • 1
    This solution will use tons of memory for a large array. Only pointing that out for the questioner. – gman Apr 01 '19 at 23:09
  • @andrew I can't think of a situation were a forEach would be "cleaner" than a for though ... – Jonas Wilms Apr 01 '19 at 23:12
  • To clarify I meant that using a reduce would be cleaner than a for loop. I shouldn't have mentioned forEach that was a bad example (I was just randomly pulling out a looping function with a callback). – Andrew Apr 01 '19 at 23:27
  • But yes thanks for the comments, I ended up running out of memory as you guys suggested might happen, so I'll go with a regular for loop. – Andrew Apr 01 '19 at 23:27
1

If you prefer methods rather than loops (which I usually do just aesthetically) you could pull in a third party module I maintain called async-af.

Among other things, it provides an asynchronous-friendly sequential forEach:

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

AsyncAF([0, 1, 2, 3, 4]).series.forEach(async () => {
  console.log('waiting');
  await sleep(1000);
});
<script src="https://unpkg.com/async-af@7.0.14/index.js"></script>

Of course, you could also just use a simple for...of loop:

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

(async () => {
  for (const _ of [0, 1, 2, 3, 4]) {
    console.log('waiting');
    await sleep(1000);
  }
})();

As for why Array.prototype.forEach doesn't work the way you'd expect, take this overly simplified implementation (here's a fuller one):

const forEach = (arr, fn) => {
  for (let i = 0; i < arr.length; i++) {
    // nothing is awaiting this function call
    fn(arr[i], i, arr);
    // i is then synchronously incremented and the next function is called
  }
  // return undefined
};

forEach([0, 1, 2, 3, 4], async () => {
  console.log('waiting');
  await delay(1000);
});

As you can see, Array.prototype.forEach synchronously calls the given callback function on each element. That's why you see all five waiting logs almost immediately. See this question for more information.

Scott Rudiger
  • 1,224
  • 12
  • 16
1

First of all, for...of is the way to go.

However if you die hard want a .forEach method, you can do something like this:

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

/* Create your own async forEach */
Array.prototype.asyncForEach = async function asyncForEach(callback, ctx){
  const len = this.length;
  for (let i = 0; i < len; i++)
    await callback.call(ctx, this[i], i, this);
};

[0, 1, 2, 3, 4].asyncForEach(async function(n){
  await sleep(1000);
  console.log(n);
});
yqlim
  • 6,898
  • 3
  • 19
  • 43