⌛
並列実行した Promise で throw されても全てハンドルしたいときの方法(allSettled, finally, etc...)
要諦
以下のアプローチが主な解決方法となるだろう
-
Promise.allSettled
で全ての Promise の解決を待ち、解決された値を用いて処理を行う(動機的後処理) -
Promise.all
に渡した各処理にPromise.prototype.{then,catch,finally}
を定義し、適切に後処理を行う(非同期的後処理) - 各処理を中断可能に実装し、 catch された時点で中断命令を送る(中断)
Thanks to @uhyo_
ハンドルしたいだけなら allSettled である必要はない旨のご指摘を頂いたため、本文趣旨を変更しました。
詳細
-
Promise.all
の返却する Promise は何かしら 1 つでも throw された時点で Reject され解決される。- このとき他の Promise は中断されない
- 浮いたまま実行され続けている (fulfilled に
1-2
が2-0
の後に push されていることに注目) - 当然 reject もされる (rejected に
1-3
がある)-
Promise.all
の catch 節で取りこぼしている- ログ漏れ、復帰処理、切断処理等々が懸念される
-
- 浮いたまま実行され続けている (fulfilled に
- このとき他の Promise は中断されない
-
Promise.allSettled
では fulfilled / rejected に関わらず(いくつ throw されても)全ての Promise が解決されるまで待つ- 成功、失敗を問わず、
Promise.all
に渡した Array の順序で結果の Array が返却される。 - 結果の型は
{ status: 'fulfilled', value: T } | { status: 'rejected', reason: any }
である (ref:typescript/lib/lib.es20xx.promise.d.ts
)- TypeScript では status の値を判別することで、 value あるいは reason のどちらが付随しているか推論される
- PromiseSettledResult<T> 型はグローバルから参照できる
- 残念ながら IE11 には未対応
- polyfill として promise.allsettled が es-shims より提供されている
- 成功、失敗を問わず、
-
Promise.prototype.{then,catch,finally}
を用いることで、Promise.all
でハンドルできなくとも、自身で後処理が行える- 特に finally は実行が保証されるため、プロセス終了前の処理にも使用できる
備考
-
Promise.all
の各処理内でPromise.reject
を実行すると一番上の Promise を reject するので注意が必要 -
Promise.all
の各処理内でPromise.resolve
に Error Object を渡す手法はPromise.allSettled
を独自実装していることとほぼ同様になるため、Promise.allSettled
が推奨されると考えられる
Promise.allSettled
でのサンプルコード (リポジトリ)
const { setTimeout } = require('timers/promises')
const values = [true, false, true, false, true]
let started = []
let fulfilled = []
let rejected = []
async function main () {
console.log('--- 1 started ---')
const results1 = await Promise.all(values.map(async (v, idx) => {
started.push(`1-${idx}`)
await setTimeout(1000 * idx)
if (v) {
fulfilled.push(`1-${idx}`)
return v
}
rejected.push(`1-${idx}`)
throw new Error(`Error at ${idx}`)
})).catch((e) => {
// catched only 1 error
console.error('error1: ', e.toString())
})
console.log('result1: ', results1) // <- undefined
console.log('--- 1 ended ---') // 1000 * 1 sec later
console.log('--- 2 started ---')
const results2 = await Promise.allSettled(values.map(async (v, idx) => {
started.push(`2-${idx}`)
await setTimeout(1000 * idx)
if (v) {
fulfilled.push(`2-${idx}`)
return v
}
rejected.push(`2-${idx}`)
throw new Error(`Error at ${idx}`)
}))
// catched all errors
console.error('error2: ', results2.filter((r) => r.status === 'rejected').map((r) => r.reason.toString()))
// all fulfilled results
console.log('result2: ', results2.filter((r) => r.status === 'fulfilled').map((r) => r.value))
console.log('--- 2 ended ---') // 1000 * 4 sec later
console.log('started', started)
/*
* started [
* '1-0', '1-1', '1-2',
* '1-3', '1-4', '2-0',
* '2-1', '2-2', '2-3',
* '2-4'
* ]
*/
console.log('fulfilled', fulfilled)
/*
* fulfilled [ '1-0', '2-0', '1-2', '2-2', '1-4', '2-4' ]
*/
console.log('rejected', rejected)
/*
* rejected [ '1-1', '2-1', '1-3', '2-3' ]
*/
}
main().catch((e) => {
console.error(e)
process.exitCode = 1
})
Discussion