[javascript] なぜ「forEachでasync/awaitは使えない」と言われるのか

# はじめに

とあるメンバーから、表題のようなことを言われました。
どうやら以下のようなコードで、「`forEach`で`await`を使っているのに処理順がおかしくなる」とのことから、この結論に至ったそうです。

```javascript:動かないコード
async function testAsync(v) {
  await new Promise(resolve => {
    setTimeout(resolve, 100);
  });
  return v + 1;
}

const data = [];
const params = [0, 1, 2];
params.forEach(async v => {
  const res = await testAsync(v);
  console.log(res);
  data.push(res);
});
console.log(data);

// [] 1 2 3
```

彼としては`await`によって非同期処理の完了を待ち、結果を`data`配列に挿入していき、最終的に`data`配列が`[1, 2, 3]`となる想定だったようですが、実際には空配列になっています。

これを見て「うんそれはそうなるよね･･･」と思いはしたのですが、彼によるとググると同じような話題のページがいくつか引っかかったとのこと。

検索してみると確かにそれなりに件数があり、それに対する解決策などもあるのですが･･･。
そもそも**なんでこのコードで動作すると思ったんだ？**　というところが気になり始めました。

結論としては、どうやら **`await`の挙動を間違って認識している**のではないかなと。
そして **`forEach`で`async/await`が動かないというのは風評被害だ**ということです。

## 追記
コメントでも指摘いただいたとおり、この状況であれば`forEach`ではなく`map`のほうが適していますので、皆さんも配列から対になる配列を得たいなら`map`を使用してください。

# 彼の主張

彼としては

```javascript:同等なコード？
for (const v of params) {
  const res = await testAsync(v);
  console.log(res);
  data.push(res);
}
console.log(data);
```

のような「**同等のコード**にしたところ正常に動作したので、やはり`forEach`の問題なのでは？」という主張のようです。

なるほど確かに一見同等に見えますが、**これは同等ではありません。**


# どこでつまづいているのか

試しに以下のようなコードで、いくつか質問をしてみました。

```javascript
(async () => {
  async function sampleAsync(v) {
    await new Promise(resolve => {
      setTimeout(resolve, 100);
    });
    console.log(v);
  }

  console.log(1);
  await sampleAsync(2); // A
  console.log(3);
})();
```

問１　これだとどういう順番で表示される？
答１　`1` `2` `3`
これは理解していそう。

問２　コード内の***A***の`await`を外したら？
答２　`1` `3` `2`
これも大丈夫そう。

問３　動かないコードって、問２のようなことになってるよね？
答３　？？？
おや？

問４　`await`ってどういう認識してる？
答４　`await`したところで処理を止めて、`Promise`の結果を待つ
一見合ってはいる。

問５　じゃあ答２が `1` `3` `2` なのはなぜ？ `sampleAsync`内で`await`してるよ？
答５　それは別の関数内でのことだから･･･あっ！
どうやらここで理解してくれたようです。

彼が提示した「動かないコード」では、`forEach`の**コールバック関数内で**`await`を行っていますが、対して「同等だと主張するコード」では、ただ`for`ループ内で行っています。
この違いが、こと`await`の挙動としては同等ではないという理由です。[^1]
[^1]:それ以外に「非同期処理が直列/並列」という違いもありますが、主題ではないので省きます。

# なぜ勘違いしたのか

どうやら彼は、`await`について「`Promise`の結果が出るまで、後続の処理を待つもの」**というだけの、ふわっとした**認識だったようです。

これ自体は完全な間違いではないのですが、正確には**関数スコープ内での**後続の処理を待つものです。
[MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/await#%E8%A7%A3%E8%AA%AC)にもちゃんと

> await 式は async 関数の実行を一時停止し、 Promise が決定される（すなわち履行または拒否される）まで待ち、履行された後に async 関数の実行を再開します。

と記載されてます。
一時停止する範囲はあくまで`async`関数内のみであり、その他の関数スコープに影響は及ぼしません。
及ぼしたらもはや非同期関数として取り扱えないですし。

この「一時停止する範囲」が恐らくクセモノで、「動かないコード」では引数に直接関数式を使用しているため、別関数であるにも関わらずあたかも一連の処理の塊に見えてしまい

```javascript
params.forEach(async v => {
  const res = await testAsync(v);
  // testAsyncが解決したらここに到達するよね（これは正しい）
  console.log(res);
  data.push(res);
});
// awaitしてるから、全て解決するまでここに到達しないよね（これは間違い）
console.log(data);
```

のような勘違いが発生してしまったようでした。

# おわりに

同様の問題に直面したネット上の方々が、彼と同じ理由でつまづいたのかどうかはわかりません。
勘違いはしておらず、「`forEach`に`async`関数を送っても、`forEach`自体がコールバックを`await`で待たないからダメだ」という意味合いなのかもしれません。[^2]
[^2]:ただそれなら`await Array.forEach(...)`ができない、になるように思うのですが、ざっと見た感じでは見当たらず。
`Array.forEach`の戻りは`void`なので、そうすれば期待通りに動くわけではありませんが。

ただいずれにしても、`async`な関数を送れば、その関数自体はきちんと非同期で動作しているわけですし、「`forEach`で`async/await`が使えない」としてしまうのは、ちょっと誤解を生むよなあと思った年の瀬でした。

# おまけ

せっかくなので、多少「直感的に書けるような」サンプルも置いておこうと思います。

```javascript
Array.prototype.forEachAsync = async function(callback, thisArg) {
  const promises = [];
  this.forEach(function(...args) {
    promises.push(callback.call(this, ...args));
  }, thisArg);
  return await Promise.all(promises);
}
```

```javascript
async function sampleAsync(v) {
  await new Promise(resolve => {
    setTimeout(resolve, 1000);
  });
  return v + 1;
}

const data = [];
const params = [0, 1, 2];
await params.forEachAsync(async v => {
  const res = await sampleAsync(v);
  console.log(res);
  data.push(res);
});
console.log(data);

// 1 2 3 [1, 2, 3]
```
[^3]
[^3]:prototype汚染を避けたければ、ただの関数にして引数で配列を送りましょう。