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段階のため今回はスルーしました。時期がきたら調査してみようと思います。