Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mj-hd/a1278b3bfe8b24c00f5c5946ac72b736 to your computer and use it in GitHub Desktop.
Save mj-hd/a1278b3bfe8b24c00f5c5946ac72b736 to your computer and use it in GitHub Desktop.
20210408_エラーを捨てるなんてもったいない!

エラーを捨てるなんてもったいない!

先日新しく作ったプロジェクトが、要件的にもエラーハンドリングをしっかりしていく方針だった。

しっかりしたエラーハンドリングってなんだろってところだけど:

  1. 下の層で起こりうるエラーをすべてハンドリングしていること
  2. プログラム内で対処できるエラーを区別できること(リトライ、UIを通してユーザにレポート、エラーレポーティング)
  3. 正確なスタックトレース、エラー情報ができるだけ失われないこと

あたりかなと考えた。
「1. 下の層で起こりうるエラーをすべてハンドリングしていること」これがまずJavaScriptでは厳しい。
GoやRustなど、エラーを返り値として返却する文化がある言語ではだいぶ良いのだが、JavaScriptはエラーをthrowしてtry/catchする。
throw, try/catch を使ったエラーの伝搬はシンタックスに現れず、例えば型定義やエラーハンドリングの漏れも検知できない。
こうなると見知らぬ地の底から未知のエラーが飛んできて、そのままユーザの顔面に突き刺さりアプリケーションがクラッシュすることも。
縦横無尽に任意のエラーが飛び交う環境では「2. プログラム内で対処できるエラーを区別できること」も全く達成できる気がしない。
いったい何のエラーが飛んでくるか分からないのだから、事前に対処することもできない。
ひとまず console.error する、エラーレポーティングに投げていたこともあったが、何の手も加えていないエラーは、
デバッグに必要な情報が欠けていることが多く、エラーレポーティングサービスの管理面を見てもノイズだらけ。
一つのエラーが複数行の修正コミットに変換されることが理想だが、そうは叶わずそっ閉じする日常の開発風景だった。

エラーは修正の種であるからして、この状況にしてしまうのはマズイ。しっかりハンドリングしていこう。

まずはJavaScript界隈のエラーの歴史的闇と戦っていかなくてはならない。
カスタムエラーという、エラーをベースとした独自のエラーを定義し、catch 節で instanceof を用いて適切にハンドリングできる機能が備わっているのだが、これが微妙。
環境依存が多く、秘伝のたれのようなおまじないをたくさん付けないとまともに使えない。どうしてこうなった。
ここで活躍したのが、ts-custom-errorというライブラリ。このあたりの闇を隠蔽しつつTypeScriptネイティブにエラーを扱える。
これでやっとまともにエラーハンドリングできるようになった。
だがこれだけだとまだ満足できない。カスタムエラーを受け取った層では、エラーから復帰できなかった場合再度throwすることになるのだが、
この時エラーが廃棄され、新しいエラーに作り直されてしまう。
これではせっかくのスタックトレースもエラーメッセージも台無しだ。修正の種を投げ捨ててしまうに等しい。
ええい何とかならんか、ということで活躍したのがtypescript-chained-error。(もっといいライブラリもあるはず)
エラーをラップしつつ新しいエラーを生成し、スタックトレースもきれいに合成することができる。しかも前述のts-custom-errorも内包しているので、これ一つあれば鳥が二羽落とせる。
エラーチェーンを使うことで、1.2.3.がだいぶ達成に近づいた。
2. エラーの対処については、Golangでいうところの errors.As/Is を実装することでかなり楽になる。
エラーが特定のエラーを内包しているかどうかを判定するのが errors.Is で、特定のエラーを取り出すのが errors.As。どちらもちょろっと再帰関数を書けば実装できる。
これにより、エラーの中から対処可能な根本原因のエラーを見つけ、対処することができるようになる。
最後に、「throw を見たら上の層では必ず try/catch で包み、その層で定義されたエラーでラップし、対処不可能であれば再度throwする。」
というルールを決め、全箇所で try/catch instanceof throw を埋め込んでいった。
そしてエラーレポーティングにはSentryというサービスを用いているが、これがなかなか相性が良く、ラップされたエラーを一つづつ関連エラーとして並べてくれる。
しかも一つ一つスタックトレースも表示されており、ここだけ見れば「どの外部システムがどういうエラーを出し、それに対してアプリケーションの各層がそういう対応をしたのか」が一画面で把握できる。
発生経路が違うが、同じメッセージのエラーも、ラップしているのできちんと別エラーとして管理ができて幸せ。
ここまでデバッグ情報がそろっていれば、あとは直すだけ。ようやく平静を取り戻した。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment