JavaScript で 非同期処理 + リトライ の仕組みを作ったら意外とややこしかったのでメモ。
実装
function retry(func, onError) {
function _retry(e) {
return onError(e)
.catch((e) => {
throw e
})
.then(func)
.catch(_retry)
}
return func().catch(_retry)
}
-
func
- 失敗したときに再試行したい何らかの非同期関数を指定する (Promise を返す必要がある)
- resolve するとその結果が返る、reject すると
onError
実行後にリトライされる
-
onError
-
func
が失敗した際に、再試行の前に実行したい何らかの非同期関数を指定する (Promise を返す必要がある) - resolve するとリトライされる、reject すると例外を投げて止まる
-
(2024.01 追記) TypeScript + async/await で書いてみるとこんな感じ? ※ 動作未確認
async function retry<T>(
func: () => Promise<T>,
onError: (e: Error) => Promise<void>
): Promise<T> {
async function _retry(e: Error): Promise<T> {
await onError(e)
try {
return await func()
} catch (e) {
return await _retry(e as Error)
}
}
return await func().catch(_retry)
}
使用例
基本形
失敗したら1秒待ってから再試行を無限に繰り返すパターン
// 失敗したときに再試行したい何らかの非同期関数
async function f() { ... }
// setTimeout を Promise で包んだだけのやつ
function sleep(msec) {
return new Promise((resolve) => window.setTimeout(resolve, msec))
}
const result = await retry(f, (e) => {
console.error(e)
console.log('sleep 1sec and retry')
return sleep(1000)
})
キャンセル
再試行するかどうかをユーザに確認して、キャンセルした場合はデフォルト値 (この場合 null
) を結果として返すパターン
// 失敗したときに再試行したい何らかの非同期関数
async function f() { ... }
let canceled = false
const result = await retry(
() => (!canceled ? f() : Promise.resolve(null)),
(e) => {
console.error(e)
canceled = !window.confirm('try again?')
return Promise.resolve()
}
)
試行回数制限
再試行の回数制限を超えたらエラーを投げるパターン
// 失敗したときに再試行したい何らかの非同期関数
async function f() { ... }
// setTimeout を Promise で包んだだけのやつ
function sleep(msec) {
return new Promise((resolve) => window.setTimeout(resolve, msec))
}
const maxRetry = 3
let retryCount = 0
const result = await retry(f, (e) => {
if (retryCount++ < maxRetry) throw e
console.error(e)
console.log('sleep 1sec and retry')
return sleep(1000)
})
補足
キャンセルや試行回数制限も実装に含めてしまえればよかったのだけれど、パッと綺麗に作れなさそうだったので、使う側でやってしまったほうがシンプルじゃん、となりこうなった。