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

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