
ブログをNext.js14(App Router)でリファクタリングしました
TypeScript/ JavaScriptのPromiseの並列処理メソッド『all, race, allSetteled, any』について、実際のコードと図を用いて分かりやすく解説します。
目次
Promise型をawaitするコードは、非同期処理を1つずつ待ってから順番に実行する『直列型』の処理です。
それに対して『all, race, allSetteled, any』は非同期処理を同時に、複数実行するためのメソッドです。
公式ドキュメントには『Promiseの並列処理メソッド』と説明されています。ですので、この記事でも『並列処理』のメソッドとして説明します。allやraceは前からありますが、allSetteledやanyは割と最近追加されました。
シングルスレッド上の処理なので厳密に言うと並列処理ではありませんが、小難しいことは最初は気にしなくてかまいません。気になる方は『JavaScript 並列処理 マルチスレッド』などで検索してみてください。
この記事を読むにあたり、基本的なPromise型やasync, awaitの使い方やresolve, rejectについてご存じのない方は次の記事を参考にしてください。
『all, race, allSetteled, any』は次の2種類に分けられます。
例えばA, B, Cの非同期処理を同時に走らせ、終わる順番がC→A→Bだとします。
①のメソッド(all/ allSettled)は終わる順番に関わらずA, B, Cすべての結果を取得しますが、②のメソッド(any/ race)は一番早く終わるCの結果しか取得しません。
また、①, ②にそれぞれ2つずつメソッドがありますが、その違いは『エラー発生時』の挙動の違いにあります。このことに関しては以下で詳しく解説していきます。
どのメソッドも、同時に実行したい非同期処理(Promise型)を配列にして引数に渡します。ここでは非同期処理を3つ並列に実行することを考えます。
Promise.all([非同期処理1, 非同期処理2, 非同期処理3])
Promise.allSettled([非同期処理1, 非同期処理2, 非同期処理3])
Promise.any([非同期処理1, 非同期処理2, 非同期処理3])
Promise.race([非同期処理1, 非同期処理2, 非同期処理3])
all, allSetteldの場合は非同期処理1,2,3の『全ての結果』が欲しいので、各結果に対応する要素を配列のデストラクチャリングで用意する必要があります。
const [result1, result2, result3]
= await Promise.all([非同期処理1, 非同期処理2, 非同期処理3])
const [result1, result2, result3]
= await Promise.allSettled([非同期処理1, 非同期処理2, 非同期処理3])
どのメソッドもPromise型を返すので、await句をつける必要があります。
一方、any, raceの場合は非同期処理のうち『1つの結果』のみが欲しいので、配列ではなく単独の値を用意します。
const result = await Promise.any([非同期処理1, 非同期処理2, 非同期処理3])
const result = await Promise.race([非同期処理1, 非同期処理2, 非同期処理3])
基本的な文法が分かったところで、それぞれの使い方と意味について順番に解説していきます。
非同期処理の具体的なコードとして、次節の解説からsetTimeoutを用いたPromiseを扱います。Promiseがresolveするのかrejectするのか区別しやすくするため、次のように、『setTimeoutで待つ秒数』と、『resolveするかrejectするかを選択するモード』を引数で渡せる関数を作成して、それを例に説明していきます。
例えば、3秒でresolveする関数,2秒でrejectする関数は次のように使えます。
promise(3, "resolve") // 3秒でresolve
promise(2, "reject") // 2秒でreject
この関数の具体的な実装は次の通りです。
/**
* setTimeoutでresolveかrejectを選択できる関数
* @param sec setTimeoutで待つ秒数
* @param mode resolveするかrejectするかを文字列リテラルで指定
* @returns Promise型の値
*/
const promise = (sec: number, mode: 'resolve' | 'reject')
=> new Promise<string>((resolve, reject) => {
setTimeout(() => {
mode === 'resolve' ? resolve(`resolved: ${sec}`) : reject(`Rejected: ${sec}`)
}, sec * 1000)
})
次のようにtry…catch分を使って、rejectされた場合にもきちんとメッセージが表示されるようにします。
;(async () => {
try {
const result1 = await promise(3, 'resolve')
console.log(result1)
const result2 = await promise(2, 'reject')
} catch (err) {
console.error(err)
}
})()
[LOG]: "resolved: 3"
[ERR]: "Rejected: 2"
それでは実際に並列処理を行うメソッドについて見ていきます。
allは、同時に実行する複数の非同期処理の『全ての結果』が欲しい場合に利用しますが、複数の非同期処理のうち1つでもrejectされると全体がrejectになりエラーとなります。以下の2つのケースが考えられます。
同時に実行した非同期処理が、それぞれきちんとresolveされて値を返します。
const all = async () => {
try {
const [p1, p2, p3] = await Promise.all([
promise(2, 'resolve'), //A
promise(1, 'resolve'), //B
promise(5, 'resolve') //C
])
console.log(p1, p2, p3)
} catch (err) {
console.error(err)
}
}
all()
出力
[LOG]: "resolved: 2", "resolved: 1", "resolved: 5"
値が返されるタイミングは、最も時間がかかった非同期処理がresolveされた時点です。上のコードのpromise最も時間がかかるのはCの5秒なので、このallが終わるのは5秒後です。
allは複数の処理のうち、どれか1つがrejectされた瞬間に全体がrejectされエラーとなります。
先ほどのコードで、2秒待つ非同期処理(Aのpromise)をrejectに変えてみます。
const all = async () => {
try {
const [p1, p2, p3] = await Promise.all([
promise(2, 'reject'), //A
promise(1, 'resolve'), //B
promise(5, 'resolve') //C
])
console.log(p1, p2, p3)
} catch (err) {
console.error(err)
}
}
all()
出力
[ERR]: "Rejected: 2"
この処理結果を時系列順に考えます。実行から1秒後にBのpromiseがresolveされます。しかし2秒後にAのpromiseがrejectされ、その瞬間にエラーとなりcatch文に移行します。5秒後に実行予定だったCのpromiseは実行されません。
allSettledはallと似てますが、途中でrejectされても、最後まで非同期処理を実行します。その代わり、返される各値は、『成功したか、失敗したか』のステータス情報が付与された、PromiseSettledResult型の値になります。
実際のコードと実行結果を見るのが早いでしょう。
const allSettled = async () => {
try {
const [p1, p2, p3] = await Promise.allSettled([
promise(2, 'reject'),
promise(1, 'resolve'),
promise(5, 'resolve')
])
console.log(p1, p2, p3)
} catch (err) {
console.error("error")
}
}
allSettled()
結果
[LOG]: {
"status": "rejected",
"reason": "Rejected: 2"
}, {
"status": "fulfilled",
"value": "resolved: 1"
}, {
"status": "fulfilled",
"value": "resolved: 5"
}
このように、各logにはstatusプロパティで『resolveかrejectか』の情報が付与され、resolveの場合は『value』プロパティにresolveの結果が、rejectの場合は『reason』にエラーメッセージが表示されます。
すべてが最後まで実行されるのでこのコードの実行時間は5秒で、エラーが発生しないのでtry…catch文で囲む必要もありません。
raceは、非同期処理のうち、1番最初にresolveかrejectされる値を1つ返します。つまりresolveかrejectかに関係なく、最も早く終わった結果のみを返します。
const race = async () => {
try {
const p = await Promise.race([
promise(1, 'reject'), //A
promise(2, 'resolve'), //B
promise(5, 'resolve') //C
])
console.log(p)
} catch (err) {
console.error(err)
}
}
race()
結果
[ERR]: "Rejected: 1"
この例では最も早く終わるのはAのpromiseなので、Aのpromiseがresolveならresolve、例のようにrejectならばエラーを返します。
最も早くresolveしたpromiseを返します。raceと似てますが、anyはrejectされたpromiseは返しません。
次の2つのケースが考えられます。
少なくとも1つresolveする場合、最も早くresolveしたpromiseを返します。
const any = async () => {
try {
const p = await Promise.any([
promise(1, 'reject'), //A
promise(2, 'resolve'), //B
promise(5, 'reject') //C
])
console.log(p)
} catch (err) {
console.error(err)
}
}
any()
結果
[LOG]: "resolved: 2"
Aのpromiseが最も早く終わりますがrejectなので無視し、2番目に終わるBのpromiseがresolveなので、これが採用されます。結果としてこの処理は2秒で終わります。
すべてresolveされない場合はエラーとなり、エラーメッセージ『All promises were rejected』が表示されます。
const any = async () => {
try {
const p = await Promise.any([
promise(1, 'reject'),
promise(2, 'reject'),
promise(5, 'reject')
])
console.log(p)
} catch (err) {
console.error(err)
}
}
any()
結果
[ERR]: All promises were rejected
anyは最初にresolveされるまで実行されるので、どれもresolveされないということは全ての非同期処理を待つことになります。結果としてこの処理は5秒かかります。
シェア!
関連記事
最新の記事
カテゴリー一覧
アーカイブ