for await of を使うとEslintに怒られるから、Promise.all()を使う
はじめに
TypeScript + Eslint で開発中にfor await of
を使って、非同期関数を同期的にループで実行しようとしたら、Eslintに怒られました。
その際の、解決策として、[1]Promise.all()
で解決する方法をご紹介します。
訂正
解決策は、僕の憶測が入ってしまっていたため、適切なものではなかったことが判明したので、修正します。
厳密には、非同期に実行されるべきものを同期的にループで実行しようとしていることそのものをEslintに怒られていました。
以降の記述では、まるでPromise.all()
が同期的な処理を行っているような表現をしていますが、Promise.all()
は同期的に処理を実行するという記述はどのドキュメントにもありませんでした。m(_ _)m
前提
以下の関数が定義されていることとします。
async function createUser(userId: number, userName: string) {
︙
︙
}
async function deleteUser(userId: number) {
︙
︙
}
怒られる例
// 同時に複数のユーザを同期的に作りたいとする
const USER_DATA_LIST = [
{ userId: 1, userName: "User1" }
{ userId: 2, userName: "User2" }
{ userId: 3, userName: "User3" }
{ userId: 4, userName: "User4" }
{ userId: 5, userName: "User5" }
]
for await (const userData of USER_DATA_LIST) {
await createUser(userData.userId, userData.userName)
}
ループ内でそもそもawait使うなって怒られます。
解決策
以下のように記述することで怒られなくなります。
Promise.all
// 同時に複数のユーザを同期的に作りたいとする
const USER_DATA_LIST = [
{ userId: 1, userName: "User1" }
{ userId: 2, userName: "User2" }
{ userId: 3, userName: "User3" }
{ userId: 4, userName: "User4" }
{ userId: 5, userName: "User5" }
]
Promise.all(
USER_DATA_LIST.map(userData => createUser(userData.userId, userData.userName))
)
Promise.all()
は、非同期関数の配列を受け取ります(実態としてはプロミスを返す関数等の配列)。そこで、データ元の配列をmapしつつ、非同期関数を返すようにすることで、スマートに非同期処理の繰り返し実行が可能となります。同期処理ではありません。
スプレッド構文で、イミュータブルにするのも忘れずに。[2]
応用編(誤った記述のため削除しました)
Promise.all
でトランザクションっぽいことをしたい、という記述をしていましたが、上記でもあります通り、同期的な実行ではなく非同期的な実行であるため、トランザクションのような記述を簡単に書くことはできません。
番外編
Promise.allSettled
途中でいずれかの関数が失敗しても、残りの関数を実行したい場合は、Promise.allSettled()
を使用します。
こちらのメソッドでは、すべての関数が実行完了するのを待って、結果を判定します。
// 同時に複数のユーザを同期的に作りたいとする
const USER_DATA_LIST = [
{ userId: 1, userName: "User1" }
{ userId: 2, userName: "User2" }
{ userId: 3, userName: "User3" }
{ userId: 4, userName: "User4" }
{ userId: 5, userName: "User5" }
]
Promise.allSettled(
USER_DATA_LIST.map(userData => createUser(userData.userId, userData.userName))
)
.then(results => {
console.log(results) // すべての関数から返された結果の配列
})
// errorはThrowされず、resultsの中で、エラー理由等が確認できる。
Promise.race
ここまで紹介してきたメソッドはすべて、同期的に関数を実行するものでしたが、非同期でまとめて実行させることもできます。
Promise.race()
は、最も早く完了した関数の結果を返します。
ぱっと用途は思いつかないですが、覚えておくとどこかで役に立つかもしれません。
// 同時に複数のユーザを同期的に作りたいとする
const USER_DATA_LIST = [
{ userId: 1, userName: "User1" }
{ userId: 2, userName: "User2" }
{ userId: 3, userName: "User3" }
{ userId: 4, userName: "User4" }
{ userId: 5, userName: "User5" }
]
Promise.race(
USER_DATA_LIST.map(userData => createUser(userData.userId, userData.userName))
)
.then(result => {
console.log(result) // 最も早く終わった関数の結果が返ってくる
})
.catch(error => {
console.log(error) // 最も早くエラーがthrowされた関数のエラーが返ってくる
})
まとめ
今回は、Promise.all()
と関連する、Promise.allSettled
、Promise.race
について紹介しました。
ぜひコーディンで採用して、スマートなコードを書いてみてください。
最後まで読んでいただき、ありがとうございました。
-
petamorikenさんにご指摘いただき、訂正します。 ↩︎
-
petamorikenさんにご指摘いただき、削除しました。 ↩︎
Discussion
いくつか気になる点がありますので指摘させてください。
for-await-of
についてJavaScript には
Iterable
と呼ばれるインターフェースがあり、それを満たしているものについてはスプレッド構文やfor-of
を使ってループ処理をすることが出来ます。配列はそれを満たしているものの一つです。一方で
AsyncIterable
というインターフェースもあります。Iterable
では値を順に取り出す際に全て同期的に取得することが出来ましたが、逆に全て非同期に取得する場合がこちらです。イメージとしては動画のストリーミング再生で、決まった順番のものを少しずつ取得しにいくような感じでしょうか。こちらはfor-await-of
でループ処理をします。最初の悪い例として
for-await-of
を使われていますが、配列はIterable
ですのでfor-of
と勘違いされているかと思います(Iterable
からAsyncIterable
へ変換可能なためfor-await-of
が使えてしまっていますが、普通はそうしません)。Promise
についてJavaScript の
Promise
は作った時点で非同期の処理が実行されています。つまり Async Function を実行した時点で既にその処理は走っています。何故 ESLint がループ内の
await
を禁止しPromise.all
を使うことを勧めるかというと、前者だと直列に、後者だと並行に実行されることになるからです(前者はまさしくAsyncIterable
と同じことをしています)。同時に並行に実行できるものはなるべくそうしたほうがパフォーマンスが良くなるためPromise.all
を使うことを勧めているわけです。つまりこれは誤りです。非同期函数(Async Function)の配列ではなく、非同期函数が実行されその結果を待つ
Promise
の配列を受け取ります。あくまで全てが成功した、もしくは一つ失敗したタイミングをはかるためのものであって、他のものが実行されなかったり中断されるようなことはありません。イミュータブルについて
イミュータブルは不変という意味です。そもそも変更できないものについて使われる言葉です。
スプレッド構文ではあくまでコピーを作っているだけですので、イミュータブルという言葉は使われません。
また配列の
Array#map
というメソッドは配列から新しい配列を作るものになっているため、ここにスプレッド構文はなくていいかなと思います。指摘は以上となります。技術記事を書くことはとても素晴らしいことだと思うのでこれからも是非続けていってください。以下の記事が役に立つかもしれません。読んでいただきありがとうございました。
貴重なご意見ありがとうございます。
改めて調べ直しまして、お手数かと思いますが、不明な点について質問させていただきます。
お時間ありましたらご回答いただきたいです。
また、いただきましたご指摘をもとに、記事の方更新しよう思います。
for-await-of
についてMDNのドキュメントを改めて確認しましたが、
とありますように、ただの配列でも理論上は、動作可能かと理解していました。また、Eslint実行外では、通常に動作しているという認識でこれまで使用していました。
暗黙的に、
Iterable
からAsyncIterable
へ変換されてしまうという記述はMDNを参照したところ見つかりませんでした。どのドキュメントを参照されたのでしょうか?ご教授いただきたいのですが、例えばあるデータの配列をもとに繰り返し処理を実行したい場合(かつ同期的に実行したい場合)は、以下のように
for of
で記述することが可能、ということで僕の理解正しいでしょうか?上記の書き方が可能であり正の場合は、僕が記事内で提示している悪い例は、僕が勘違いで書いていますので、訂正させていただきます。
Promise.all
についてこちらも改めてMDNを確認させていただきました。
MDNの文章中には、
とあるように、確かにご指摘の通り、渡されたプロミスという言葉が使われているので、関数ではなく
Promise
が渡されていること理解できました。この時、
というのは、
Promise.all
では、Catchされた以外のPromise
がどうなっているのかを把握することができないということで理解正しいでしょうか?イミュータブルについて
こちらについては、
Array.map
に関して僕が仕様を勘違いしていました。新しい配列を返す場合は、あえてコピーを作成する必要はないですね。
記事内で訂正させていただきました。
返信遅れましたが、ご確認お願いします。
for-await-of
についてJavaScript の言語仕様である ECMAScript に記述されています。JavaScript エンジン内部で暗黙的に使われている内部オペレーションの
GetIterator
にて、第二引数の hint に async を指定した際CreateAsyncFromSyncIterator
が使われていることが確認できます(for-await-of
で使われます)。はい
for-of
で記述することが可能です。非同期函数の中の話なので同期的な実行と言えるのかわからないところですが、少なくともその函数は最初にawait
に到達するまでは同期的に実行されると言えると思います(非同期函数はawait
式を使っていない部分は全て同期的に実行されます)。Iterable
にfor-await-of
を使用した場合については MDN にも記載されていますが、イテレーターリザルトの値がPromise
だった場合に違いがあるものになっています。USER_DATA_LIST
にPromise
が含まれない今回の例ではfor-of
もfor-await-of
も結果としては違いはないですね。Promise.all
についてその通りです。以下のコードを実行してみるとわかりやすいかもしれません。
なお余談ですが非同期関数内では
Promise.all
に対してもawait
することが出来ます。個人的にはPromise#then
やPromise#catch
メソッドを使うよりはこちらを使いますね。JavaScript の非同期処理全般については以下の連載記事がかなり詳しいです。入門記事へのリンクもありますので確認してみてください。
すべての質問に対してご丁寧に対応いただきましてありがとうございます!
アウトプットのリスクとして、この記事を閲覧した人に勘違いを与えてしまう可能性がにはあることを改めて認識できました。
より、詳細にドキュメントを調べてから、アウトプットをしていこうと思います!
そもそもの前提として、
for await of
がEslintに怒られる原因だと勘違いしていましたが、非同期処理を同期的に繰り返す行為そのものに対して、Eslintは怒ってくれてたということですね。。そういう意味では、脳死で
Promise.all
を使おうという方針自体が間違っていると思いますので、この記事の修正をさせていただこうと思います。改めまして、こんな記事でも丁寧に対応いただきまして本当にありがとうございます。