ビューティフルWebコード

美しいWebサイトのコーディングについて説明をしていきます。

エラー処理(例外処理)を共通化しよう!

エラー処理はとても重要!でも大変なので、ルールを決めよう!

エラー処理(例外処理)は非常に重要な処理です。
真面目に処理しようとしたら、正常系のコードよりもエラー処理のコードの方が多くなっていくと思います。
どのプログラム言語もtry-catchなどの例外処理が言語仕様として組み込まれていると思います。
ですが、それぞれのエンジニアがそれぞれ思い思いにエラー処理を実装していたのでは、とてもいい品質のコードはできません。
効率よく開発するためにエラー処理にもルールを設ける必要があります。

エラーの種類を大別する!

自分は、エラーの種類は以下の2種類があると考えています。

  • 自己復帰できるエラー
  • 自己復帰できないエラー(システムエラー)

これらを以下のように定義します。

自己復帰できるエラーとは

障害ではない。ログレベルで言うならWARN。
ユーザが自分の力で復帰できるのが望ましい。
やり直しをさせることで、処理を完了することができる。

  • URLのパラメータ改ざんによりDB検索をしたがデータが存在しなかった。
  • Cookieが改ざんされた。
  • セッションが切れた。
  • などなど

自己復帰できないエラー(システムエラー)

障害。ログレベルで言うならERROR、FATAL

  • インフラ障害(ネットワーク、DB)
  • バグ

エラー処理の実装を開発者にどう伝えるか

自分がリーダーだったら、こうします。

エラー処理はするな!

え?って思われるかもしれません。
要件や、詳細設計としてフローチャート、シーケンス図レベルで記載されるロジックエラーについては、それぞれのエンジニアに任せましょう。
でも、それ以外のエラー処理に関しては一切エラー処理をするな!と言います。

すべてのエラーを共通エラー処理に集めてしまい、そこでエラー処理を行います。

なぜ、そうなるかを考えていきましょう。

実際にどういうエラーが起こるか検討してみる

/users/1234 (/users?id=1234) というURLについて考えてみます。

以下の処理が行われるものとします。

  • UsersControllerのIndexActionメソッドが呼び出される。
  • DBからid=1234のユーザを検索する。
  • 検索結果をViewに渡して表示する。

この中で、どういうエラーが発生するか、異常系を考えてみます。

  • a. id=1234のユーザが存在しなかった。
  • b. そもそもバグがあって、プロダクトコード内で実行中に例外が発生した。(ぬるぽetc)
  • c. 設定が間違って(DBの接続文字列が変わって)いてDBに接続できなかった。
  • d. ネットワーク障害で、DBに接続できなかった。
  • e. その他、現段階では想定していないのエラーが発生するかもしれない。

ここで考察です。
a〜eのエラーについて、どういうエラー処理をさせたいでしょうか。
要件がある場合は従う必要があります。
要件が提示されなかったから何もしなくていいのかというとそうは行きません。
少なくとも共通エラー画面を用意して、エラーを通知すべきです。

考えられるエラー処理を次に記載します。

a. id=1234のユーザが存在しなかったは、自己復帰できるエラー

ユーザが意図的にURLを改ざんし、存在しないidを入力してきた場合や、
ユーザのURLをブックマークしていたけど、ユーザが退会してしまっていた、といったことも考えられます。

以下のエラー処理が考えられます。(要件に従う必要があります。要件がなければ提案する事象です。)

  • 共通エラー画面に遷移させる。(例外をthrowして共通エラーハンドラに処理を任せる。)
  • 404 NotFound扱いにする。(サイト内の404表示の手続きに従う。)
  • 該当するユーザは存在しないか、退会しています。という独自画面を表示する。(Action内独自処理を入れる)

共通エラー画面に遷移させるのは少し乱暴です。共通エラーは障害時しか表示してはいけないからです。
また、共通エラーハンドラではエラーログを出力するため、ログ監視サービスが稼働してる場合、アラートが上がります。
もし、共通エラーハンドラに任せる場合、これは障害ではないよ!っていう独自の例外の型を準備して、それをthrowし、エラーログを出さないようにする必要があります。

その他は自己復帰できないエラー(システムエラー)

プログラムのバグ、ネットワーク障害、設定ミス(バグ)すべてユーザには関係のないサイト側の問題です。
ユーザはどうすることもできません。
これらは共通エラーページを表示するべきです。個々にエラー処理をしてはいけません。

フレームワークのエラー処理の機能を理解しよう!

どのフレームワークにも必ずエラー処理の機能が準備されています。
また、フレームワークで処理されなかった例外は、Webサーバー(Apache, IIS)などが500エラーを返します。

エラー処理機能を細かく切り出すと以下のようになります。
それぞれでハンドルされなかった場合、次の機能にエラー処理を任せます。

  • Actionメソッド内でのエラー処理(try-catch)
  • Actionに設定したフィルタまたはアノテーションによるエラー処理
  • Controllerによるエラーイベントの処理
  • Controllerに設定したフィルタまたはアノテーションによるエラー処理
  • フレームワークの共通(グローバル)エラー処理
  • Webサーバーによる500処理

理想論を言えば、

  • そのエラーがURL固有(Action固有)であればAction内で処理すべきです。
  • そのエラーがController固有であればControllerのエラーイベント処理で処理すべきです。
  • そのエラーがサイト共通であれば、フレームワークの共通エラー処理に任せるべきです。
  • Webサーバーの500処理は起きてはいけませんが、フレームワークが処理仕切れない例外が発生することもあるので、共通エラー画面に表示するhtmlをサーバーの500にも設定しておくべきです。404も同様にサーバーにも設定しておきましょう。

これでも、結構複雑ですね(笑)。これをいろんなエンジニアの独断に任せたらまた大変なことになりそうですね。

つまり、要件にあるURL固有のエラー処理以外は、

エラー処理はするな!

となるのです。
共通エラー処理についてはリーダーや、メインプログラマ、1人に任せておきましょう(笑)

共通エラー処理としてやっておくといいこと

エラー画面が表示されると、エンドユーザは不安になります。
緊急であればサイト運営に問い合わせなどを行うことになるでしょう。
保守運用を依頼されている場合、障害対応となります。
障害対応はなるべく時間をかけずに簡潔に行えるようにしておくと、運用コストを削減できるでしょう。
(保守費用が月額で決まっている場合、時間がかかればかかるほど、エンジニアのボランティア作業になってしまう)

自分がよく実装するのは以下のような仕組みです。

  • GUIDなどで一意のお問い合わせID(エラーID)を生成します。
  • 例外オブジェクトの内容、メッセージをお問い合わせIDと一緒にエラーログに出します。
  • エラー画面を表示するのですが、ここでお問い合わせIDを一緒に出力します。メーラーを起動する仕組みであればお問い合わせIDをタイトルなどに含めてあげるといいでしょう。

ここで注意なのですが、
エラー処理では、エラーを起こす可能性のある仕組みを実装してはいけません。
たとえば、お問い合わせIDを生成した場合、その内容をDBに記録する!なんて実装をしてはいけません。
DB障害でエラーハンドラーに飛んできた場合、エラーハンドラでDB書き込めないでしょ?

また、ZABIXなどの監視ツールを使いエラーログを監視するといいですね!

実際にエラーが起こったらどうするの?

共通エラーハンドラで出力したエラーログを確認します。
それが、コードのバグなのか、インフラエラーなのかを切り分けます。
コードのバグであればバグ対応してください。今後そのエラーはなくなりサイトの品質が上がります。
インフラのバグであれば、インフラ対応をしてください。冗長化構成をするなど、障害に強いインフラを目指してください。