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

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

Recent Posts

Categories

Tool 日々 Web Biz Net Apple MS ことば News Unix howto Food PHP Movie Edu Community Book Security Text TV Perl Ruby Music Pdoc 生き方 RDoc ViewCVS CVS Rsync Disk Mail FreeBSD Cygwin PDF Photo Zebedee Debian OSX Comic Cron Sysadmin Font Analog iCal Sunbird DNS Linux Wiki Emacs Thunderbird Sitecopy Terminal Drawing tDiary AppleScript Life Money Omni PukiWiki Xen XREA Zsh Screen CASL Firefox Fink zsh haXe Ecmascript PATH_INFO SQLite PEAR Lighttpd FastCGI Subversion au prototype.js jsUnit Apache Trac Template Java Rhino Mochikit Feed Bloglines CSS del.icio.us SBS qwikWeb gettext Ajax JSDoc Rails HTML CHM EPWING NDTP EB IE CLI ck ThinkPad Toy WSH RFC readline rlwrap ImageMagick epeg Frenzy sysprep Ubuntu MeCab DTP ERD DBMS eclipse Eclipse Awk RD Diigo XAMPP RubyGems PHPDoc iCab DOM YAML Camino Geekmonkey w3m Scheme Gauche Lisp JSAN Google VMware DSL SLAX Safari Markdown Textile IRC Jabber Fastladder MacPorts LLSpirit CPAN Mozilla Twitter OpenFL Rswatch ITS NTP GUI Pragger Yapra XML Mobile Git Study JSON VirtualBox Samba Pear Growl Mercurial Rack Capistrano Rake Win RSS Mechanize Sitemaps Android JavaScript Python RTM OOo iPod Yahoo Unicode Github iTunes God SBM friendfeed Friendfeed HokuUn Sinatra TDD Test Project Evernote iPad Geohash Location Map Search Simplenote Image WebKit RSpec Phone CSV WiMAX USB Chrome RubyKaigi RubyKaigi2011 Space CoffeeScript Nokogiri Hpricot Rubygems jQuery Node GTD CI UX Design VCS Kanazawa.rb Kindle Amazon Agile Vagrant Chef Windows Composer Dotenv PaaS Itamae SaaS Docker Swagger Grape WebAPI Microservices OmniAuth HTTP 分析基盤 CDN Terraform IaaS HCL Webpack Vue.js BigQuery Middleman CMS AWS PNG Laravel Selenium OAuth OpenAPI GitHub UML GCP TypeScript SQL Hanami Document SVG AsciiDoc Pandoc DocBook Develop Jekyll macOS Node.js Vite Heroku Transformer AI Data Cloud Wasm