2018-10-07

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.

参考

About

例によって個人のなんちゃらです