JavaScriptのPromiseでretryするいくつかの方法
JavaScript で Promise はそこそこ扱えるようになったけど、 retry をどうするか曖昧なままだったのでちょっと整理する。
まずダメなやつ
普通に同期的なコードの場合、やり方はいろいろあると思うけど、素朴にはこんな感じに書けると思う。
function s() {
return Math.random() > 0.5
}
let retry = 3
let result
while ( retry > 0 ) {
result = s()
if ( result ) {
break
} else {
retry--
}
}
console.log(result)
実際には retry 付きで呼ぶ function に閉じると思うけど、だいたいこんな感じかなーと思う。
で、Promise でもこれを元になんとかしてみようとすると、うまくいかない。s() を Promise を返すようにしてみたとして retry 部分を書き換えるとこんな感じになると思う。
while ( retry > 0 ) {
result = s()
s()
.then(() => {
break
}).catch(() => {
retry--
})
}
console.log(result)
しかし上のコードを Node.js 6 で実行すると以下のように怒られる。
break
^^^^^
SyntaxError: Illegal break statement
そりゃそうだ
Promise は callback 地獄を解消するものと思われているけど、実際には引数に対してではないけど結果に対して callback を与えて処理をチェインする仕組みであり、ということは上に書いた break の scope は Promise を呼んでいる外側とは異なる、while ループと異なるものになっている。
つまり、break するものがない場所で break しようとしている。
Promiseのchainで書く特徴を生かしてシンプルに呼び出す側がretryする例
retry 回数などの抽象化を一切無視すると以下の形になる。
s().catch((err) => {
return s()
}).catch((err) => {
return s()
.then((r) => {
console.log(r)
})
.catch((err) => {
console.error(err)
})
Promise が rejected になったらもう一度同じ function を call するだけ。これで 3回試行できている。
途中 Promise をわざわざ return しているのは、こうしないと Node.js で UnhandledPromiseRejection の警告が出るからだけど、確かに返しておく方が理に適っていると思う。
cf. 【JavaScript】Promiseのリトライ処理をちゃちゃっと - エムティーアイ エンジニアブログ
呼び出される側がretry込みで実行する例
上のやり方は呼び出す側の処理に retry を入れてしまっているため、呼び出し箇所が複数あった場合に DRY でなくなってしまう。できれば呼び出される側で retry 込みで処理してほしい。
これは再帰させるとすっきり書ける。
function p(retry = 3) {
return new Promise((resolve, reject) => {
let r = Math.random()
if ( r >= 0.5 ) {
if ( retry > 0 ) {
p(--retry).then(resolve).catch(reject)
} else {
return reject(false)
}
} else {
resolve(r)
}
})
}
こうすると p() を呼ぶ側は p() を普通の Promise のように扱っているにも関わらず、中で任意の回数 retry させることができる。
※ retry 回数を引数に与えて再帰で処理していく部分の説明は割愛する。
キモは.then(resolve).catch(reject)
これがないと結果の「クチ」を「戻す」ことができない。つまり retry に入ったら resolve も reject も外からは分からないままなんとなく終了してしまう。
そう、ここで指定している resolve, reject こそが最初の return new Promise() に渡ってくるものであり、呼び出す側の
p()
.then()
.catch()
を実現するために必要なものなのだ。ということで
.then(resolve).catch(reject)
を与える必要がある。
通常の再帰は自分自身を call する際に return しながら戻ってくるのだが、Promise は渡ってくる resolve, reject を then, catch に与えることで「chainableな状態を維持する」形になるようだ。
気持ち悪いが、よく考えると納得はいく。ただこれはイディオムとして覚えてしまうのが早いように思う。
cf.
- javascript - Promise Retry Design Patterns - Stack Overflow
- A few general patterns for retries using promises