宙に浮いたPromiseを作らない
宙に浮いたPromiseというのは、await
のつけれられていない非同期処理の呼び出しがある関数(もしくはPromise
をreturn
していない関数)です。
具体的には以下のようなものです
async function sendRequest(){
await postA()
postB()
}
このsendRequest()
関数をawaitして呼び出してみると、postA()
関数はきちんと処理完了まで待ってくれますが、postB()
は待ってくれなくなります。
await sendRequest() // => postAの完了を待機するが、postB()の完了は待機してくれない
// 本当はpostB()の実行を待って実行したかった処理
doSomething()
このような関数を作ってしまうと、非同期処理のハンドリングが非常に困難になってしまうので、基本的にはawait
をつけるなり、returnで返すなりして、きちんと呼び出し元からハンドリングできるようにしましょう。
並列実行しつつPromiseを制御する
Promiseを並列実行しつつ、きちんと制御するにはPromise.all
やPromise.allSettled
を使います。
これらの機能を使うことで、宙に浮いたPromise関数の呼び出しを作らずに、複数の非同期処理を非同期に実行することができます。
await Promise.all([
postA(),
postB()
])
// postAとpostBが並列に実行され、両方が正常終了する、もしくは、どちらかがエラーになったら処理が終了し、ここが実行される
doSomething()
Promise.allSettled
は割と最近の構文なので使えないプロジェクトもあることに注意
await Promise.allSettled([
postA(),
postB()
])
// postAとpostBが並列に実行され、両方の処理が正常に終了するかエラーになるかし、すべての結果が決まったあと、ここが実行される
doSomething()
非同期処理の繰り返し処理
非同期処理の繰り返す処理は少し注意が必要です。よくある悪い例は以下です。
[user1,user2].forEach(async (user) => {
await sendUser(user)
})
上記のようにforEach
やmap
を使ってPromise関数を引数に指定した場合、JSは引数に指定された関数をawaitをつけずに実行します。
なので上記は以下と等価です。Promiseが宙に浮いてしまっている悪い例ですね
(() => await sendUser(user1))()
(() => await sendUser(user2))()
// 少しわかりにくいが以下とだいたい同じ
sendUser(user1)
sendUser(user2)
一方for文を使って以下のように書くこともできます。
for (const user of [user1,user2]) {
await sendUser(user)
}
for文はforEachと違い、関数の呼び出しを行うわけではないので、上記は以下と等価です。
await sendUser(user1)
await sendUser(user2)
間違っているわけではないのですが、この書き方だとすべてのループが直列に実行されるので、ループの回数が多い場合は性能に悪影響が出る可能性があります。このため、ESLintではno-await-in-loopというルールで禁止されていることが多いです。
すべてのループの非同期処理を並列に実行して、すべての実行結果の完了を待つことを考えるのであれば、以下のようなコードを書くのが良いと思います。forEachではなくmapを使っているところがポイントです。
// ここではallSettledを使っているが、環境や目的によってはallでもほぼ同じ使い方ができる
await Promise.allSettled(
[user1,user2].map((user) => {
return sendUser(user) // returnでPromiseを返しているが、`await sendUser(user)`のようにしても良い
})
)
これは以下のコードと等価です
await Promise.allSettled([
sendUser(user1),
sendUser(user2)
])