hbsnow.dev

async/await を forEach で使ったらハマった話

JavaScriptのasync/awaitをforEachで使ったらハマった話 - HackMD
JavaScriptのasync/awaitをforEachで使ったらハマった話 - HackMD favicon https://hackmd.io/UsWcutr_RGyh1wKYcZOdIA
JavaScriptのasync/awaitをforEachで使ったらハマった話 - HackMD

上記のスライドを社内で発表したのでそのときのまとめです。サンプルコードは下記にあります。

GitHub - hbsnow-sandbox/js-async-await
Contribute to hbsnow-sandbox/js-async-await development by creating an account on GitHub.
GitHub - hbsnow-sandbox/js-async-await favicon https://github.com/hbsnow-sandbox/js-async-await
GitHub - hbsnow-sandbox/js-async-await

前提

const timer = (delay) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(`timer ${delay}ms`);
      resolve(delay);
    }, delay);
  });
};

[100, 300, 200].forEach(async (delay) => {
  await timer(delay);
});

上記のコードで期待する結果にならなかったことを相談されたことが発表の経緯です。

$ # 期待した結果
$ timer 100ms
$ timer 300ms
$ timer 200ms
$ # 実際の結果
$ timer 100ms
$ timer 200ms
$ timer 300ms

[100, 300, 200] で順次実行されて欲しかったのですが実際の出力では並列で実行されてしまっています。

順次処理をするためには

(async () => {
  for (const delay of [100, 300, 200]) {
    await timer(delay);
  }
})();

for...of で書くのが、おそらくもっともシンプルでわかりやすいはずです。

しかし、状況によっては使いたくても使えないということもあります。例えばAirbnbのJavaScript Style Guideのようなコーディングルールを採用している場合には for...of が使用禁止されてときなどでしょう。

そういった場合には then を使用することで解決できます。

import { timer } from "./timer";

let promiseChain = Promise.resolve();
[100, 300, 200].forEach((delay) => {
  promiseChain = promiseChain.then(() => timer(delay));
});

then を使うのが微妙と感じるのであれば reduce でも実現可能です。

[100, 300, 200].reduce(async (accumulator, delay) => {
  await accumulator;
  return timer(delay);
}, Promise.resolve());

ただこの方法だと何が目的でこういう記述になっているのか、すぐにわかりにくいのではないかと感じます。この理由についての話はreduce の使いどころに書いています。

並列処理して全部終わるまで待つ

今回のスライドの趣旨とはあまり関係ありませんが、普通に並列処理して全部終わるまで待ちたいのであれば、Promise.all()map を使います。

(async () => {
  await Promise.all([100, 300, 200].map(async (delay) => await timer(delay)));
})();