Promise.reject()で処理は止まるがそのタイミングは「期待」通りとは限らない

まとめ

止まることは止まるが、思ったタイミングで止まるかどうかは分からないので、それ前提で作る必要がある。

例えば以下は思い違い。

  • 最初の一つが rejected になったら、その一つめで必ず止まる

実際には

  • 最初の rejected が Promise.all に伝わる段階ですでにいくつも並列で処理が走っている可能性があるので、rejected 以降にエラーがバンバン発生することはあり得る

もっと言うと rejected が伝わった段階で中の複数の Promise の結果は fulfilled と rejected が混ざっている可能性もあるので、全体の rejected を retry すると fulfiled になった Promise をもう一度実行する場合もある。

ということは例えば何かの POST 処理を Promise.all で回しつつ retry を考慮している場合、すでに成功しているデータが複数回 POST される可能性がある。

もっと言うと、

そこで何かの拍子に unique 制約に引っかかって逆にエラーがあとから起きたりもする。

ということ。

非同期並列面倒くせぇ…。

何で悩んでいたのか

Promise.all(Array.map(e => Promise))

なコードがあった。これを呼び出す function foo() があった。

function foo(data) {
  Promise.all(data.map(e => Promise))
}

この foo() を直接読んでテストしている時は最初の一つが reject になった段階で止まっていた。で、

一つめが rejected になったら止まると思い込んでしまった

しかし、次に

function bar() {
  ..
  ..
  foo()
  .then(() => {
    ..
  })
  .catch((err) => {
    ..
  })
  ..
}

の bar() でテストすると、

大量にエラーが起きてしまったので、「止まらないじゃん!」と、また勘違い

「何かコードの書き方を間違えているから止まらないのでは…」とありもしない原因を探る旅に出てしまったのでした…。

正解は、Promise.all([Promise, ..]) の実行に至るまでに払っている実行コストの間で中の Promise が並列に実行を開始してしまい、結果、

呼び出し元が rejected でストップしたタイミング以降にいくつもエラーが起きて当然の状況になる

つまり

一つめの rejected で止まるのも、まったく止まっていないように見えるのも、どちらも正しい!!!

非同期並列難しい!

何が難しいって同期的な処理を期待しているシステムと繋げるのが難しい!

愚痴まじりで言うと Node.js をこれまで避けてたのはやっぱり正しかったなぁと思った。それは動作が安定しているかどうかとか、高速かどうかとか、そういうことじゃなくて動作の考え方そのものが難しいので、カジュアルな変更を前提にしたコードには向いていないなぁという意味。記法だけを見れば callback 地獄は Promise によって解決されたかのように見えるが、動作については非同期並列という特徴はそのままなので、伝統的な、同期的で排他ロック可能なコードを扱う脳との切り替えおよび整合性の確保が難しい。整合性の確保は複数のサービスの連携が前提となるマイクロサービスにおいては必須の要件になってくる。

今後 FaaS, サーバレス化を進めるに当たってはここはキモになってくるし、

  • スケール、非同期前提のインフラに載せつつ変更の少ない部分に使っていく
  • 同期的な仕組みの側も非同期並列の仕組みから繋げる際のエラーの起き方を知っている必要がある
  • モニタリングの意味も同期的な仕組みとちょっと変わってくる

かなぁということを思っている。

More