[小ネタ]forEachではasync/awaitが使えない

javascriptのforEachではasync/awaitが使えないので、for...of,Promise.all()を使用する方法を記載しました。
2021.09.28

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

IoT事業部の木村です。

DynamoDBからGetItemをするという処理を書いていて、forEachの中でasync/awaitが効かないということに気づかず非常に多くの時間を使ってしまいました。大変基礎的でお恥ずかしい内容ですが、今後の備忘のために書き記しておきます。

Array.prototype.forEach()

実行環境

node.js v12.22.1

準備

「test」という名前のDynamoDBテーブルを作成して、idだけの単純なデータを1000件入れておきます。

動かなかったコード

ということで、動かなかったコードは以下の通りです。 forEachは非同期処理に対応した設計をされていないようです。

const AWS = require('aws-sdk');
AWS.config.update({ region: 'ap-northeast-1' });
const documentClient = new AWS.DynamoDB.DocumentClient();


(async () => {
  const ids = [...Array(1000).keys()]
  const response = []
  ids.forEach(async (id) => {
    const item = await documentClient.get({
      TableName: 'test',
      Key: {
        id: ('000' + (id + 1)).slice(-3),
      }
    }).promise()
    response.push(item.Item)
  })
  console.log(response)
})()

対応方法

何点か考えられますが、以下2点ご説明させていただきます。

for...ofを使用する方法

以下の方なコードです。DynamoDBのDocumentClientの生成までは上記のコードと同じなので省略しています。

なお、こちらの処理は全て直列で実施されます。一つ一つのループでの処理が完了してから次の処理が始まります。

(async () => {
  const ids = [...Array(1000).keys()]
  const response = []
  for (const id of ids) {
    const res = await documentClient.get({
      TableName: 'test',
      Key: {
        id: ('000' + (id + 1)).slice(-3),
      }
    }).promise()
    response.push(res)
  }
  console.log(response)
})()

Promise.all()を使用する方法

以下の方なコードです。Array.prototype.map()でpromiseの配列を作成して、Promise.all()で解決しています。

なお、こちらの処理は並列で実施されます。

(async () => {
  console.time('promise_all');
  const ids = [...Array(1000).keys()]
  const response = await Promise.all(ids.map(async (id) => {
    return documentClient.get({
      TableName: 'test',
      Key: {
        id: ('000' + (id + 1)).slice(-3),
      }
    }).promise()
  }))
  console.log(response)
  console.timeEnd('promise_all');
})()

まとめ

forEachで非同期処理がうまく扱えないということで、for...ofを使う方法と、Promise.all()を使う方法をご紹介しました。 大変基礎的な内容で恐縮ですが、どなたかの役に立てれば幸いです。

ちなみに、1000件のデータをローカルから取得する平均時間(5回ずつ)は以下のような感じでした。 なので、並列で処理が進んでいい場合はPromise.all()の方が処理速度では有利ですね。

方法 実行時間
for...of 43648.527ms
Promise.all() 1064.920ms