2018-07-01

JavaScriptのPromiseではまずcatchから書け

以下は Promise 全般について詳しい者が書いているわけではなく、単に JavaScript の実装に基づいて分かったことを書いている程度の話です。

※ ただし、Promise.all() や Promise.race() を catch から書くのはいろいろまずそうなのでやめた方がよいです。

Promiseについて説明されないこと「catchが何をcatchしているのか」

少なくとも JavaScript の実装では

  • Promise が reject したもの
  • catch 以前に Promise call 後に throw された例外

の両方を catch してしまう。

何が困るのか

特にサンプルとして世の中に出てくるコードの大半は残念ながら

Promise
.then(func)
.catch(func)

の形をしている。ここに罠がある。具体的には

then の中の例外がすべて catch に吸い込まれてしまう。

この Promise はこういう挙動をするはずだ、例えば「Fetch API の catch はネットワークエラーに対応しているはずだ」という理解でコードを書き始めるとしょっちゅう意図しない catch が起き、then を途中ですっ飛ばしてしまうという挙動に悩まされる。しかも console には何も出ない。

これは以下のような簡単なコードで確認できる。

fetch('/success', {method: 'POST'}).
  then((response) => {
    throw new Error();
    console.log(response)
  }).
  catch((err) => {
    console.log(err)
    console.log('network error')
  })

ネットワークエラーが起きていないにも関わらず network error と console には表示される。(この場合はその前に throw new した Error も表示される。)

まずcatchしろ

上に対する解決策は単純で、

「まず reject を catch で処理しろ、そのあとに then を書け」

になる。こうすれば then の中のミスを問題なく普通に runtime に伝えることができる。

※ はじめ「さもなくば捨てろ」と書いたが、Promise の引数は resolve, reject の順で必須だし、Node.js 8 では catch がないと UnhandledPromiseRejectionWarning が出まくるので勧めない。

Promiseは本当に面倒くさいと思った、

Promise は callback 地獄に対する回答のように見えるが、

  • 値の Proxy(取得タイミングはお任せ)
  • Promise をもとに「処理を記述する」

の複数の役割を持っており、単なる Proxy なら単に await すれば値が取得できるだけのように見えるが、Promise ベースで catch/then で「状態を待ち、処理を記述していく」際には、「状態こそ不変かもしれないが Promise の reject もその他の例外も『記述順によっては丸ごと catch される』」という繊細な挙動をする」。

この辺が実際にものを作っていく際に本当に難しいなぁというか、異常に面倒くさい感じがしましたとさ。

どうしても then の中が複雑になる場合はそこだけ切り出して別な名前空間で書いた方がいいんでしょうね。渡ってくる response が何かは分かってるわけで、テストに十分なデータのパターンを作って Promise の外で普通に TDD で作ります、自分なら。

おしまい。

参考

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