26
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

forEach内でawaitはできない(JavaScript、TypeScript)

Last updated at Posted at 2022-07-19

先日、TypeScriptでの実装で、すごく基本的なところでハマってしまったので、メモとして残しておきます。

TypeScript(JavaScript)でforEach内でawaitを記述したら、エラー(「'await' 式は、非同期関数内と、モジュールのトップレベルでのみ許可されます。」)となりました。

エラーとなったコードはこんなやつです。

const millisecs = [1000, 2000, 3000]
millisecs.forEach(millisec => {
  await sleep(millisec)
  console.log(millisec)
})
console.log('forEach完了')

sleep()関数はこんなもので、引数で指定した時刻(ミリ秒)処理を中断します。

async function sleep (millisec: number): Promise<void> {
  return new Promise(resolve => {
    setTimeout(resolve, millisec)
  })
}

Visual Studio Codeのクイックフィックスを利用したところ、「含まれている関数にasync修飾子を追加します」が候補にあったので、これを選択すると下記のようにコードが修正されました。

const millisecs = [1000, 2000, 3000]
millisecs.forEach(async millisec => { // クイックフィックスによってasyncが追加された
  await sleep(millisec)
  console.log(millisec)
})
console.log('forEach完了')

なるほど、asyncの付け忘れだったかと思い納得したのですが、実行してみると下記のような予想外の結果となりました。

forEach完了
1000 // 1秒後
2000 // さらに1秒後
3000 // さらに1秒後

予想していた結果はこうです。

1000 // 1秒後
2000 // さらに2秒後
3000 // さらに3秒後
forEach完了

調べてみたところ、mdn web docsにforEach は同期関数を期待します。との記載を見つけました。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach

ということで、forEach内に渡すコールバック関数は、同期関数でないとダメなようです。

繰り返し処理で非同期関数を扱いたい場合は、下記のようにfor...ofPromise.all()を使うということになります。

const millisecs = [1000, 2000, 3000]

// for...ofを使った場合
for (const millisec of millisecs) {
  await sleep(millisec)
  console.log(millisec)
}

// Promise.all()を使った場合
await Promise.all(millisecs.map(async millisec => {
  await sleep(millisec)
  console.log(millisec)
}))

console.log('forEach完了')

ただし、Promise.all()を使った場合はコールバック関数が並列で処理されるため、結果は以下のようになります。ご注意ください(@arimoo さんご指摘ありがとうございます)。

1000 // 1秒後
2000 // さらに1秒後
3000 // さらに1秒後
forEach完了

さらに、今回はsleepする時間に差があるためコールバック関数が順番に呼ばれているように見えますが、実際にはPromise.all()の場合、3回呼ばれるコールバック関数は並列で処理されるため、順序性は保証されない点をご注意ください(@ktz_alias さんご指摘ありがとうございます)。

まとめ

今回の問題は非常に基本的ですが、結構ハマる人が多いようです。

forEachに限らず、コールバック関数がawaitされているかどうかはわからないので、コールバック関数には非同期関数を渡さないのが安全そうです。

26
14
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?