async/awaitを利用した配列ループの注意点

async/awaitを記述した関数内で配列ループを行うと、予期せぬ結果を受け取りました。

以下は、Now環境下のNode.jsの抜粋です。コードの目的はFirestoreへのデータ登録。ローカルにあるJSONファイルのキー名を配列に格納し、ルート内でループしてFirestoreのドキュメント名として利用しようとしています。各キーの配下には、実データが格納されています。

const { Router } = require("express");
const data = require("../data/data.json");

router.get("*", async (req, res) => {
  // キー名を配列として格納
	const docKeyAry = Object.keys(data);

  // 配列分ループしてFirestoreに値を格納
  await docKeyAry.forEach(async (data, index) => {
    await firestore
      .collection("sample")
      .doc(docKey)
      .set(data[docKey]);
  });
	...
  res.status(201).send("OK!");
}

実行してみるとデータは挿入できませんでした。

原因

async/await内でのforEachはうまく動作しないようです。forEachループ完了を待つことができません。今回はFaaS内でコストのかかるFirestoreへのデータ登録を行う前に201レスポンスを返してしまうため、挿入プロセスは中断されたようです。

一般的な関数もループ処理を待つことはできませんが、上の例のようにクライアントにHTTPレスポンスを返すわけではないので、関数内の処理自体は達成されます。

const base = "https://reqres.in/api/users?page="
const ary = [1, 2, 3];

async function test() {
  await ary.forEach(async (data, index) => {
    const result = await fetch(base + data);
    console.log(result.url);
  });
  console.log('complete!');
}

// complete!
// "https://reqres.in/api/users?page=2"
// "https://reqres.in/api/users?page=1"
// "https://reqres.in/api/users?page=3"
test();

awaitを無視して、ループ全体処理を待たずにcomplete!が表示されているのが確認できます。また、処理の順序は保証されません

対処方法

async/awaitで配列ループを完了まで待機させるには、for...ofを使うのがベターなようです。また、配列要素の取り出しにArray.prototype.entries()というメソッドを使います。

Array.prototype.entries()

entries()は、配列のインデックスと要素のペアになるイテレーターオブジェクトを返します。イテレーターのためnext()で次の要素にインデックスを移すことが可能です。

// Array.prototype.entriesの例

var ary = ['a', 'b', 'c'];
var iterator = ary.entries();

console.log(iterator.next().value); // Array [0, "a"]

for...of内でインデックスの要素の繰り返しを行うには、以下のような記述になります。


// for...ofでの利用例

const ary = ['a', 'b', 'c'];

for (const [index, element] of ary.entries()) {
  // 0 'a'
  // 1 'b'
  // 2 'c'
  console.log(index, element);
}

インデックスが不要なら、for (const element of ary.entries())という記述も可能です。

for…of + async/await

それではasync/await内で使用してみます。

const base = "https://reqres.in/api/users?page="
const ary = [1, 2, 3];

async function test() {
  for (const [index, data] of ary.entries()) {
    const result = await fetch(base + data);
    console.log(result.url);
  }
  console.log('complete!');
}

// "https://reqres.in/api/users?page=1"
// "https://reqres.in/api/users?page=2"
// "https://reqres.in/api/users?page=3"
// complete!
test();

期待通りの動きができました。Promise.allで並行処理を行う方法もありますが、上記のコードの方がシンプルですし、なおかつ順当に処理を行えます。

更にfor await…ofというメソッドも存在します。こちらは現在Draft段階のため今回はスルーしました。時期がきたら調査してみようと思います。