ループの中で非同期関数を呼び出す際に期待する挙動をしてくれないとき


## はじめに

この間ループの中で非同期関数を呼び出した際に期待される挙動をしてくれずどハマりしてしまいました。

非同期処理に関する理解を深めたかったため、実際に簡単なコードを書いて試行錯誤した末になんとか解決できたので、その知見を共有したいと思って記事にしてみました。（調べても解決方法が見つけられなかったのも理由のひとつです）

## ループ内で非同期でない関数を使用する場合

下記のようなコードがあるとします。

```javascript
let counter = 0

const countSync = () => ++counter

const makeLoopSync = () => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve()
    })
  })
}

makeLoopSync()
  .then(() => console.log('Start'))
  .then(() => {
    const list = Array(3).fill(null)
    list.forEach(() => {
      const count = countSync()
      console.log(count)
    })
  })
  .then(() => console.log('Done'))
```

このコードの期待される実行結果は下記です。期待する挙動をしてくれていますね。

```
Start
1
2
3
Done
```

## ループ内で非同期関数を使用する場合

### 期待する挙動をしてくれないコード

ところが、`forEach()` 内で使用されている `countSync()` が非同期関数だった場合はどのような結果になるでしょうか。

`countSync` を `countAsync` に変更し中身も非同期にしてみました。

```javascript
let counter = 0

const countAsync = () => new Promise(resolve => setTimeout(() => { resolve(++counter) }))

const makeLoopSync = () => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve()
    })
  })
}

makeLoopSync()
  .then(() => console.log('Start'))
  .then(() => {
    const list = Array(3).fill(null)
    list.forEach(() => {
      countAsync()
        .then(count => console.log(count))
    })
  })
  .then(() => console.log('Done'))
```

実行結果はこうなります。

```
Start
Done
1
2
3
```

さて、上のコードの実行結果を期待されるものにするためにはどうしたらよいでしょうか。

筆者はまず `async/await` を使う必要がありそうだなと思いました。（実際には必ずしも `async/await` を使う必要はありませんでした）
そして `async/await` を使って直感的に書くならこうかなと思い、下記のように書き換えてみました。

```javascript
makeLoopSync()
  .then(() => console.log('Start'))
  .then(() => {
    const list = Array(3).fill(null)
    list.forEach(async () => {
      const count = await countAsync()
      console.log(count)
    })
  })
  .then(() => console.log('Done'))
})
```

結果はこうでした。

```
Start
Done
1
2
3
```

挙動は上の `async/await` を使用しないパターンと同じでした。内部的にも同じことが行われているのだと思われます。

### 期待する挙動をしてくれるコード

~~いろいろ試してみた結果、最終的には該当箇所を下記のように変更したら期待する挙動をしてくれました。~~

嘘でした。以下2つのコードは NG パターンです。`countAsync()` の timer をいじるとうまく動かないことを確認しました。
（@u39ueda さん ご指摘ありがとうございます・・！）

```javascript
makeLoopSync()
  .then(() => console.log('Start'))
  .then(() => {
    return new Promise(resolve => {
      const list = Array(3).fill(null)
      list.forEach(async () => {
        const count = await countAsync()
        console.log(count)
        resolve()
      })
    })
  })
  .then(() => console.log('Done'))
```

~~またはこうでしょうか。（`async/await` を使わないパターン）~~

```javascript
makeLoopSync()
  .then(() => console.log('Start'))
  .then(() => {
    return new Promise(resolve => {
      const list = Array(3).fill(null)
      list.forEach(() => {
        countAsync()
          .then((count) => console.log(count))
          .then(() => resolve())
      })
    })
  })
  .then(() => console.log('Done'))
```

~~いずれにせよ Promise を一枚かまさないといけないわけですね。~~

Promise.all を使用すると安定した挙動になります。（ただし並列処理）
（@u39ueda さん ありがとうございます。）

```javascript
makeLoopSync()
  .then(() => console.log('Start'))
  .then(() => {
    const list = Array(3).fill(null)
    return Promise.all(list.map(async () => {
      const count = await countAsync()
      console.log(count)
    }))
  })
  .then(() => console.log('Done'))
```

`new Promise(resolve => ) ...` をかます必要がないのですっきりします。

※ たしかループの中も直列で処理できる方法があったと思うので、（Array#reduce を使ったやつ）いずれ追加します。

## おわりに

~~resolve させるのが **`forEach()` の外ではない**のが個人的には解せなかったです。~~まだまだ修行が足りませんね・・・。

ただ、やはりなにかの理解を深める（学ぶ）方法として、自分で手を動かして試行錯誤するというのは非常に効果的だなとあらためて感じました。スポーツと似ていますね。

## 関連

- [Promise と async/await の理解度をもう1段階上げる](https://qiita.com/SotaSuzuki/items/f23199e864cba47455ce)