.NET の例外処理 Part. 4

というわけで、前回まで 3 回(+α)に渡って .NET の例外処理の適切な書き方について解説してきましたが、ここまでの解説にもまして重要なのは、

  • 表示またはロギングされた例外情報を、正確に読み取れるようになること。

です。実は例外に含まれる情報には、デバッグや障害解析に欠かせない情報が多数含まれており、これらを正しく読めるようになるだけで、以下のようなことがわかることが多いのです。

  • どこで例外が発生したのか
  • アプリケーションの内部構造がどんな形になっているのか
  • 例外がどんなシチュエーションで発生したのか
  • 上記から、アプリケーションで障害が発生した理由、またはバグの内容

もちろん、例外に含まれる情報だけですべてのことがわかるというわけではありませんが、例外情報を正しく読めるようになるだけで、かなりのことがわかるようになるのも事実です。これについて解説します。

なお、本エントリについては、前半部と後半部に分けて解説します。

  • 前半部では、すべての開発者に知っていただきたい、例外情報の基本的な読み方
  • 後半部では、多少経験を積んだ開発者に知っていただきたい、応用的な例外オブジェクトの使われ方

自分のスキルレベルに合わせて以下の情報をお読みいただければと思います。

[例外情報の基本的な読み方]

  • 例外オブジェクトに含まれる 3 つの情報
  • 例外情報の基本的な読み取り方

[応用的な例外オブジェクトの使われ方]

  • 例外クラスの分類
  • 例外オブジェクトのメッセージ情報の使われ方
  • 例外オブジェクトのネスト
  • 例外オブジェクトのカスタムプロパティ

では、以下に順に解説していきましょう。

[例外オブジェクトに含まれる 3 つの情報]

ここまで解説してきたように、.NET における例外は、アプリケーションエラーやシステムエラーが発生したことを表現するために使われるものでした。

image

さて、この例外オブジェクトは、それが発生した場所から上位のモジュールに伝搬されてきますが、この例外オブジェクトの中には、発生した障害に関する重要な情報が多数含まれています。このため、集約例外ハンドラでこれを記録しておき、事後的にこの例外情報を解析すると、さまざまなことがわかるようになります。例外オブジェクトに含まれる情報の中でも、特に重要な情報は以下の 3 つです。

  • 例外オブジェクトのクラスそのもの(どんな例外クラスか?)
  • 例外オブジェクトに含まれているメッセージ情報(例外オブジェクトの .Message プロパティ)
  • 例外オブジェクトに含まれるスタックトレース情報(例外オブジェクトの .StackTrace プロパティ)

これらは、実際の例外ログを見ながら理解するのが早いでしょう。

[例外情報の基本的な読み取り方]

というわけで、ひとつ具体例を出したいと思います。今、下図のような Web アプリケーションで、ユーザ名とパスワードを入力し、ログインボタンを押下したところ、例外が発生したとします。(ここでは話をわかりやすくするために、アプリケーションの内部構造や例外が発生した場所を示していますが、実際にはこれらがわからなくても OK です。例外ログを解析すると、アプリケーション内部のモジュール構成や、どこで例外が発生したのかがわかります。)

image

今、記録された例外情報が以下のようになっていたとしましょう。

    1: 【例外クラス】
    2: System.Data.SqlClient.SqlException
    3:  
    4: 【メッセージ】
    5: SQL Server は一時停止しています。新たに接続することはできません。ユーザー '(null)' はログインできませんでした。
    6:  
    7: 【スタックトレース】
    8: System.Data.SqlClient.SqlConnection.Open()
    9: System.Data.Common.DbDataAdapter.QuietOpen(IDbConnection connection, ConnectionState& originalState)
   10: System.Data.Common.DbDataAdapter.Fill(Object data, Int32 startRecord, Int32 maxRecords, String srcTable, IDbCommand command, CommandBehavior behavior)
   11: System.Data.Common.DbDataAdapter.Fill(DataSet dataSet, Int32 startRecord, Int32 maxRecords, String srcTable, IDbCommand command, CommandBehavior behavior)
   12: System.Data.Common.DbDataAdapter.Fill(DataSet dataSet)
   13: NETWSSample.CS.BizLogic.Login.DataAccess.LoginDAC.FillUserInfoWithReadCommitted(UserInfoDataSet ds, String userId) in c:\devprojects\netwssample\netwssample.cs.bizlogic\login\dataaccess\logindac.cs:line 108
   14: NETWSSample.CS.BizLogic.Login.BizFacade.LoginBizFacade.CheckPassword(String userId, String plainPassword) in C:\DevProjects\NETWSSample\NETWSSample.CS.BizLogic\Login\BizFacade\LoginBizFacade.cs:line 27
   15: NETWSSample.CS.WebUI.Login.btnLogin_Click(Object sender, EventArgs e) in C:\DevProjects\NETWSSample\NETWSSample.CS.WebUI\Login.aspx.cs:line 74
   16: System.Web.UI.WebControls.Button.OnClick(EventArgs e)
   17: System.Web.UI.WebControls.Button.System.Web.UI.IPostBackEventHandler.RaisePostBackEvent(String eventArgument)
   18: System.Web.UI.Page.RaisePostBackEvent(IPostBackEventHandler sourceControl, String eventArgument)
   19: System.Web.UI.Page.RaisePostBackEvent(NameValueCollection postData)
   20: System.Web.UI.Page.ProcessRequestMain()

この場合には、以下のようにして例外情報を読み取っていきます。

Step.1 まず、例外クラスの種類とメッセージ情報を読む。

まず、例外情報の中の、クラスの種類とメッセージ情報を読み取ってください。これにより、おおよそどのようなトラブルが発生したのかが分かります。

上記の例の場合には、例外オブジェクトのクラスが System.Data.SqlException になっています。このことから、この例外はデータベースに対する SQL 文処理にかかわる何かしらのトラブルで発生したことが類推されます。また、メッセージ情報は “SQL Server は一時停止しています。新たに接続することはできません。ユーザー '(null)' はログインできませんでした。” となっています。このことから、この例外(障害)は、データベースサーバが一時停止状態になっていて接続できなかったために発生したことが類推されます。このため、この障害の解決方法は、SQL Server の一時停止状態を解除することである、とわかります。

……と、この例の場合には例外クラスの種類とメッセージ情報だけで原因がつかめてしまいましたが;、実際の例外ではこれだけでは十分に理由が判断できないこともあります。この場合には、さらに以下のことを行います。

Step.2 スタックトレース情報を読み取る。

スタックトレースとは、アプリケーションのメソッドの呼び出しの積み重ねのことを言います。アプリケーションの中では、あるモジュールのメソッドからあるモジュールのメソッドが呼び出され、さらに別のモジュールのメソッドが....を繰り返します。この呼び出しの連鎖関係を表現したものを、スタックトレースといいます。

スタックトレースは、通常、末尾から読み取っていきます。上記の例で言うと、8 行目の “System.Data.SqlClient.SqlConnection.Open()” から読み取るのではなく、20 行目の “System.Web.UI.Page.ProcessRequestMain()”、その次に 19 行目の “System.Web.UI.Page.RaisePostBackEvent(NameValueCollection postData)”、という順番で読み取っていきます。これらは、

20 行目のメソッドから 19 行目のメソッドが呼び出され、 19 行目のメソッドから 18 行目のメソッドが呼び出され、 18 行目のメソッドから 17 行目のメソッドが呼び出され、 …. 2 行目のメソッドから 1 行目のメソッドが呼び出され、 1 行目のメソッドの中で System.Data.SqlException 例外が発生した。

という意味を持っています。このため、このメソッドの連鎖関係を追いかけることで、例外が発生した状況をある程度つかむことができる、というわけです。

とはいえ、すべてのスタックトレースを頑張って読み取るのも大変です。このスタックトレースを読み取る場合にはコツがあって、以下のようにするとラクです。

  • まず、スタックトレースを、以下の 3 つの部位に分解する。

    ① ASP.NET ランタイムやクラスライブラリによる基盤処理部分
    ② 基盤処理部分から呼び出されたユーザアプリケーション部分
    ③ ユーザアプリケーションから呼び出されたクラスライブラリ部分

  • これにより、例外が発生したソースコードの『場所』を読み取る。

上記のサンプルの場合、20 行目~1 行目の流れをイラスト化すると、次のようになります。どこが①、②、③に対応するのかは、スタックトレースに出てくるクラス名やメソッド名をよく見るとわかります。

image

このようなイメージが頭の中に思い浮かべられるようになると、例外が発生したシチュエーションが、アプリケーションコードの中身を見なくてもある程度想像できてしまいます。この例の場合だと、

  • ボタンのクリックイベントからパスワードチェック処理が走り、
  • パスワードチェックロジックの中で、データベースからユーザ情報を呼び出そうとした。
  • このとき、DataAdapter 経由でデータを取り出そうとしたが、内部的にコネクションを開こうとしたところで失敗した。

というところまでは、アプリケーションのソースコードや内部構造などの情報を事前に持っていなくても推測できてしまうことになるわけです。

# このため、例外情報はむやみにエンドユーザに見せてはいけません。特に、そこそこ知識を持った開発者が例外情報を読むと、アプリケーション内部構造に関する情報が漏えいすることになり、セキュリティ上のリスクにつながります。

# ちなみにこのような 3 つの部位への分解については、ASP.NET アプリケーションだけでなく Windows フォームのようなアプリケーションで有用です。参考までに、Windows フォーム上で発生させた例外のサンプルをのせておきますので、この例外ログから、アプリケーションがどんな作りになっていて、どういったシチュエーションで、何が原因で例外が起きたのかを類推してみてください。

    1: 【Messageプロパティ】
    2: ユーザー 'sa' はログインできませんでした。
    3: 【StackTraceプロパティ】
    4:    at System.Data.SqlClient.SqlConnection.Open()
    5:    at VSWSSample701.CS.WinForms.DataAccess.MethodZ() in C:\DevProjects\VSWSSample701\VSWSSample701.CS.WinForms\DataAccess.cs:line 22
    6:    at VSWSSample701.CS.WinForms.Form1.button1_Click(Object sender, EventArgs e) in c:\devprojects\VSWSSample701\VSWSSample701.cs.winforms\form1.cs:line 238
    7:    at System.Windows.Forms.Control.OnClick(EventArgs e)
    8:    at System.Windows.Forms.Button.OnClick(EventArgs e)
    9:    at System.Windows.Forms.Button.OnMouseUp(MouseEventArgs mevent)
   10:    at System.Windows.Forms.Control.WmMouseUp(Message& m, MouseButtons button, Int32 clicks)
   11:    at System.Windows.Forms.Control.WndProc(Message& m)
   12:    at System.Windows.Forms.ButtonBase.WndProc(Message& m)
   13:    at System.Windows.Forms.Button.WndProc(Message& m)
   14:    at System.Windows.Forms.ControlNativeWindow.OnMessage(Message& m)
   15:    at System.Windows.Forms.ControlNativeWindow.WndProc(Message& m)
   16:    at System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)

なお、スタックトレースに関してもう一点注意すべきこととして、誤ったリスロー処理をしない、という点があります。例えば、SqlException 例外を処理する典型的なコードとして、次のような例を考えてみましょう。

    1: public bool InsertAuthors(string au_id, string au_fname, ...) {
    2:   SqlConnection sqlcon = new SqlConnection("server=.;Initial Catalog=pubs;...");
    3:   SqlCommand sqlcmd = 
    4:     new SqlCommand("INSERT INTO authors VALUES ('172-32-1176', 'White', ...)", sqlcon);
    5:   try {
    6:     sqlcon.Open();
    7:     try {
    8:       sqlcmd.ExecuteNonQuery();
    9:     }
   10:     catch (SqlException sqle) {
   11:       if (sqle.Number == 2627) {
   12:         return false;
   13:       }
   14:       else {
   15:         throw;
   16:       }
   17:     }
   18:   }
   19:   finally {
   20:     sqlcon.Close();
   21:   }
   22:   return true;
   23: }

この例においては、エラー番号 2627 の SqlException 例外(PK 制約違反)以外で発生した例外は、本当は捕捉してはいけなかった例外であるため、throw という命令により catch しなかったことにしています。しかし、この命令を throw sqle; と記述してしまうと、スタックトレースの起点がこの場所になってしまい、一部のスタックトレース情報が失われてしまいます。このため、例外を catch しなかったことにする場合には、必ず throw とのみ記述し、throw sqle; といった具合に書かないようにしてください。

 Step.3 例外ログやトレースログなどとの突合せを行う

基本的な例外情報の読み方は以上ですが、残念ながら上記の情報だけでは、以下のようなことがわかりません。

  • 誰からのリクエストに対してこの障害が発生したのか?
  • テキストボックスなどから具体的にどんな値が入力されていたのか?

スタックトレースは、メソッドの呼び出し連鎖に関する情報を保持していますが、そのときにどんなパラメータ値が渡されたのかの情報は保持していません。例外の中には、特定のパラメータ値を渡した場合や、特定の条件下でのみ発生するようなものもあり、こうしたものは、スタックトレース情報だけからでは完全に原因が追跡できない場合もあります。このため、その障害の発生条件をより詳細に掴むために追加の例外ログやトレースログを取得しておき、必要に応じて突合せを行うとよいでしょう。前回のエントリにおいて EMAB のカスタムパブリッシャとして、HTTP 関連情報をかたっぱしから出力する TraceLogFilePublisher というものを開発したのは、この問題を少しでも緩和するためのものです。

さて、以上が例外情報の基本的な読み方です。ここまでの解説からわかるように、

  • 正しく書かれたアプリケーションコードから出力される例外ログは、スタックトレースなどが適切に出力されるため、有益な情報が多数含まれている。
  • 例外情報は正しく読めるようになると、システム障害のトラブルシュートやアプリケーションバグの修正において、非常に強力な力を発揮する。

ということになります。まずアプリケーションコードにおいて、try-catch などを不要に書かないようにすること(=適切に処理すること)、そして例外が発生したときに誰かに丸投げするのではなく、その情報を読み解く努力をすることが大切です。特に後者は「訓練」みたいなところがありますので、ぜひ現場経験を積んで、例外ログをさくっと読めるようになってください。

では引き続き、例外に関する応用的な内容をいくつか解説していきましょう。以降の解説はやや難しいですが、これらを理解すると例外の使い方がより一層うまくなると思います。

[例外クラスの分類]

.NET Framework のクラスライブラリの中には、かなりたくさんの例外クラスが定義されています。代表的なものをピックアップしてみましょう。

  • ArgumentException
  • ArgumentNullException
  • InvalidEnumArgumentException
  • InvalidOperationException
  • RemotingException
  • SqlException
  • OracleException
  • ArithmeticException

と、挙げだすとキリがないのですが、.NET や Java では、こうした大量の例外を 2 つの観点から分類・設計し、使いやすい形で例外クラスを提供しています。

  • ネームスペースによる例外の利用場所・タイミングの分類
    その例外クラスを、いつどこで利用するか?
  • 継承による例外の意味的な分類
    その例外は、意味的にどのような関係になっているか?

image

もう少し突っ込んで解説してみましょう。

① ネームスペースによる例外の利用場所・タイミングの分類

まず、例外クラスは、その例外がスローされる名前空間の内で定義されています。例えば、SqlException 例外は System.Data.SqlClient 名前空間の中で定義されていますが、これは次のような理由によります。

  • この例外は、SQLServer に対するデータベース処理で発生したシステムエラーを表すためのもの。
  • つまり、System.Data.SqlClient 名前空間のクラス(SqlConnection, SqlCommand, SqlDataAdapter など)を利用したときに発生する。

※ ただし、極めて汎用性の高い例外については、システム全体で共通的に利用できるよう、System 名前空間内に定義されています。

さらに、このような名前空間による分類に加えて、例外クラスの名称自体が分かりやすいものになっていますので、これにより、その例外クラスの利用場所やタイミングなどが、名前空間と例外クラス名だけからでもかなり類推できるようになっているわけです。

そして、例外クラスの分類としてもうひとつ重要なのが、例外クラスの継承関係です。

② 継承による例外の意味的な分類

例外は、その意味的関係が継承関係により表現されています。例えば、上図の例の中の ArgumentException, ArgumentNullException, ArgumentOutOfRangeException の 3  つを取り上げてみましょう。これらは以下のようなシチュエーションで発生します。

  • ArgumentException 例外 : あるメソッドに渡された引数が、本来あってはならない値だった場合に使う例外
  • ArgumentNullException 例外 : null であってはならないパラメータに対して null が与えられた場合に使う例外
  • ArgumentOutOfRangeException 例外 : インデックスなどの引数に範囲外の値が与えられた場合に使う例外(例えば要素数 4 の int[] 配列 a に対して、a[10] などと記述した場合)

さて、この 3 つの例外の関係を考えてみると、下の 2 つの例外は、ArgumentException 例外のシチュエーションを、より細かく分類・説明したものであるといえます。このような場合には、これらの例外クラスの間に継承関係を持たせるようにします。(ArgumentNullException 例外や ArgumentOutOfRangeException 例外を、ArgumentException 例外の派生クラスとして設計・実装する)

例外クラスにこのような継承関係を持たせることのメリットは、例外クラスが継承関係を持っていると、呼び出し元が例外の捕捉粒度を自由に決めることができるようになることです。もちろん .NET の場合、呼び出し元で例外を捕捉すること自体が少ないのですが(詳細は Part. 1 を参照)、仮に例外をハンドリングするのであれば、以下のように、捕捉粒度を変えることができます。

■ 引数不正を確認するようなサンプルプログラム

※ 本来はパラメータについての事前チェックを行ってから当該メソッドを呼び出すべきなので、以下のようなコードは書くべきではありません。ですが、ここではサンプルとして示します。

image

例1. 引数が null であるためにエラーが発生した場合には特別なメッセージを出したい場合

この場合には、catch 文を分けて記述します。

    1: try {
    2:     objBizFacade.MethodInvoke(strParam1, intParam2); // メソッド呼び出しを行う
    3: }
    4: catch (ArgumentNullException ane) {
    5:     Console.WriteLine("引数として null が渡されました。");
    6: }
    7: catch (ArgumentException ae) {
    8:     Console.WriteLine("不正な引数が渡されました。");
    9: }

例2. 引数が null である場合を特に区別する必要がない場合

この場合には、catch 文を分ける必要がありません。なぜなら、ArgumentNullException は ArgumentException 例外の派生クラスなので、ArgumentException を catch すれば、ArgumentNullException もまとめて捕捉できてしまうからです。

    1: try {
    2:     objBizFacade.MethodInvoke(strParam1, intParam2); // メソッド呼び出しを行う
    3: }
    4: catch (ArgumentException ae) { // ArgumentNullException は ArgumentException でもある
    5:     Console.WriteLine("不正な引数が渡されました。");
    6: }

このため、アプリケーション/システムエラーケースを体系化できる場合には、継承関係を用いて例外クラスを設計・実装するとよい、ということになります。

しかし、上記のような例外の継承関係は、同時に危険性もはらんでいます。なぜなら、すべての例外クラスを Exception 例外で捕捉できてしまうからです。Part. 1 でも解説しましたが、以下のように Exception 例外を捕捉するようなコードは絶対に書いてはいけません。なぜなら、このようなコードを書いてしまうと、発生するすべての例外が捕捉されてしまうため、本来捕捉してはいけない例外をも捕捉してしまう危険性が生じるからです。

    1: public bool InsertAuthors(string au_id, string au_fname, ...) {
    2:   SqlConnection sqlcon = new SqlConnection("server=.;Initial Catalog=pubs;...");
    3:   SqlCommand sqlcmd = new SqlCommand("INSERT INTO authors VALUES ('172-32-1176', 'White', ...)", sqlcon);
    4:   try 
    5:   {
    6:     sqlcon.Open();
    7:     sqlcmd.ExecuteNonQuery();
    8:   }
    9:   catch (Exception e) // このようなコードは絶対に書いてはいけない!
   10:   {
   11:     return false;
   12:   }
   13:   finally 
   14:   {
   15:     sqlcon.Close();
   16:   }
   17:   return true;
   18: }

※ もちろん、リソース解放のための try-finally などではすべての例外に対する処理が必要になるため、このような場合には catch (Exception) とすることもあるのですが、アプリケーションエラーを業務エラーに変換するような場合の try-catch 記述の場合には、Exception クラスを捕捉してはいけません

いずれにしても上記のように、例外クラスは以下の 2 つの観点から分類・整理されていることを理解しておきましょう。

  • ネームスペースによる例外の利用場所・タイミングの分類
  • 継承による例外の意味的な分類

次に、例外オブジェクトが内部に含んでいるメッセージ情報について解説します。

[例外オブジェクトのメッセージ情報の使われ方]

一般に、すべての例外オブジェクトはメッセージ情報を含んでいます。このメッセージ情報には、障害解析やバグ解析に有益な情報が多数含まれていますが、このメッセージ情報をエンドユーザに対して見せてはいけません。これは、セキュリティ上の理由もありますが、もうひとつ、そもそも発生した例外をどのようにエンドユーザに通知したり見せたりするのかは、UI が個別に決定すべきことだからです。

image

例外オブジェクトのメッセージ情報は、多くの場合、集約例外ハンドラによってイベントログなどに記録され、その後の障害解析に利用されます。そしてこのメッセージ情報は障害解析を行う場合に最初の手がかりになるので、自分で例外オブジェクトを throw する場合(典型的には自爆処理により例外を throw する場合)には、障害解析を行う際に有用となると思われる情報を含めておくようにしましょう。例えば、Part.1 でも採り上げた、新規顧客登録業務における BC 部の実装を考えてみましょう。

image

    1: public RegistCustomerResult ResistCustomer(string id, string name, string mail, 
    2:                         DateTime birthday)
    3: {
    4:     if (Regex.IsMatch(id, "^[A-Z]{2}[0-9]{4}$") == false) throw new ArgumentException("id");
    5:     if (Regex.IsMatch(name, "^[\u0020-\u007e]{1,20}$" == false) throw new ArgumentException("name");
    6:     if (birthday >= DateTime.Now) throw new ArgumentException("birthday");
    7:  
    8:     // テーブルアダプタを利用
    9:     CustomersTableAdapter ta = new CustomersTableAdapter();
   10:     try
   11:     {
   12:         // INSERT 命令を実施
   13:         int affectedRows = ta.InsertCustomer(id, name, mail, birthday);
   14:         if (affectedRows != 1) throw new ApplicationException("INSERT 処理に失敗しました。");
   15:     }
   16:     catch (SqlException sqle)
   17:     {
   18:         if (sqle.Number == 2627) {
   19:             return RegistCustomerResult.DuplicateCustomerIDError;
   20:         }
   21:         else {
   22:             throw;
   23:         }
   24:     }
   25:     return RegistCustomerResult.Success;
   26: }
   27:  

もちろん上記のような実装でも問題がないのですが、できることなら以下のような実装にしておくべきです

 ■ 変更前
  
 if (Regex.IsMatch(id, "^[A-Z]{2}[0-9]{4}$") == false) throw new ArgumentException("id");
 if (Regex.IsMatch(name, "^[\u0020-\u007e]{1,20}$" == false) throw new ArgumentException("name");
 if (birthday >= DateTime.Now) throw new ArgumentException("birthday");
 if (affectedRows != 1) throw new ApplicationException("INSERT 処理に失敗しました。");
  
 ■ 変更後
  
 if (Regex.IsMatch(id, "^[A-Z]{2}[0-9]{4}$") == false) throw new ArgumentException("id : " + id);
 if (Regex.IsMatch(name, "^[\u0020-\u007e]{1,20}$" == false) throw new ArgumentException("name " + name);
 if (birthday >= DateTime.Now) throw new ArgumentException("birthday " + birthday.ToString());
 if (affectedRows != 1) throw new ApplicationException("INSERT 処理に失敗しました。" + affectedRows.ToString());
  

このように、例外オブジェクトのメッセージ情報の中に、「そのときに問題となったパラメータや実際の値や状況」に関する情報を含めておくと、例外ログのメッセージ情報を見ただけで原因が推測しやすくなることが多々あります

もちろん、こうした例外というのは通常の利用時には発生しないもの(=システムエラーなどの障害発生時、クラッキングを受けた場合、アプリケーションバグがあった場合などにしか発生しない)なので、こうした実装は 「転ばぬ先の杖」 とでも呼ぶべきものです。ですが、後から「困った!なぜこの例外が発生したのかさっっっぱりわからん!」という素敵な状況にならないようにするためにも(往々にしてよくあるのですがorz)、こうした実装をしておくことを心がけるようにしましょう。

なお、例外を自力で throw して自爆する場合に知っておくとよい Tips をいくつか書いておきます。

① throw する例外クラスの選択にはあまり凝らなくておっけー。

前述したように、例外クラスには名前空間と継承による体系的な分類があるため、throw する例外クラスを適切に選択すると、呼び出し元で捕捉粒度を変えたりすることができます。がしかし、.NET ではそもそも呼び出し元で例外を catch すること自体が基本的にはないので、逆にいえば、catch されることを想定して例外クラスを細かく選定する必要はない、ということになります。具体的には以下のようなことがいえます。

  • アプリケーションから例外を throw する場合には、Exception クラスや ApplicationException クラスの派生クラスを自力で定義し、これを throw しなければならない、と書かれていることがありますが、どうせ誰も捕捉しない(集約例外ハンドラが処理する)ので、ぶっちゃけ不要です。
  • 自爆目的であれば、基本的には ApplicationException 例外をそのまま使ってこれを throw すれば十分。(Exception 例外を直接 throw すると他の例外との区別がつかなくなるので、これはさすがにやめましょう。)
  • 引数不正による例外や、権限不足によるセキュリティ例外などに関しても、ApplicationException 例外を throw して自爆してもらって全く問題ありません。が、これらについては System 名前空間に ArgumentException 例外や SecurityException 例外が用意されているので、これぐらいは使ってあげてもよいでしょう。というわけで、上記のサンプルではこれらの例外を使っていますが、ApplicationException 例外を throw してもぜんぜん問題ないです。

なお、上記が当てはまるのは、業務アプリケーションの UI, BC, DAC などを実装する場合だけです。.NET Framework のクラスライブラリ、あるいは業務共通コンポーネントなどのように、汎用的に利用されるライブラリを設計する場合には、モジュール内から発生させた例外を、呼び出し元側で try-catch することがしばしばあります。このような場合には、throw する例外クラスの選択にきちんと留意する必要があります。(=throw する例外クラスのチョイスに気を払わなくてよいのは、業務アプリでは基本的には例外を try-catch しないから、なのです。)

② メッセージ情報を丁寧に書く必要はない。

また、例外を throw して自爆するコードを書く際には、メッセージ情報を丁寧に書く必要はありません。例えば下記の例を見てください。

 ■ 悪い例
 if (Regex.IsMatch(id, "^[A-Z]{2}[0-9]{4}$") == false) 
     throw new ArgumentException("引数として渡された id パラメータのフォーマットが不正です。期待される id のフォーマットは、^[A-Z]{2}[0-9]{4}$ ですが、このフォーマットに合致していません。具体的に渡された値は " + id + "です。");
  
 ■ 良い例
 if (Regex.IsMatch(id, "^[A-Z]{2}[0-9]{4}$") == false) 
     throw new ArgumentException("id : " + id);

上の例の場合、もしこの行にヒットして例外が発生すると、例外ログには非常にわかりやすいエラーログが出ることになります。がしかし、以下のような理由によりこれはやりすぎです。

  • 例外ログは、エンドユーザが見るものではなく、障害発生時にバックエンドの人たちが見るもの。つまり、バックエンドの人たちがわかるようなものであれば十分。多くの場合、例外ログのメッセージ情報は、主にアプリケーション開発担当者がデバッグや障害解析のために見るものなので、開発者がわかる情報であれば十分
  • そもそもこの例外が発生すること自体、普通はない。システム障害やアプリケーションクラックなどでしか発生しない「予防措置」的なコードは必須だが、その実装作業にはあまりお金をかけられない。ということは、極力簡単な実装で済ませる必要がある

このケースの場合、絶対に必須であるメッセージ情報としては、

  • どのパラメータがまずかったのか?
  • 実際にどんな値だったのか?

の 2 つだけです(ちなみに 1 点目については、例外が発生した行番号がわかればそこからどのパラメータのエラーだったのか解析することもできるのですが、さすがに面倒なのでメッセージ情報に入れた方がラクです)。よって、必要最低限の情報であるこの二つの情報のみをメッセージに含めるため、サンプルコードの下側のような実装を行うわけです。

ちなみにこのルールが当てはまるのも、業務アプリケーションの UI, BC, DAC などを実装する場合だけです。.NET Framework のクラスライブラリ、あるいは業務共通コンポーネントなどのように、汎用的に利用されるライブラリを設計する場合には、上記の「悪い例」のような実装を取らなければなりません。なぜなら、この例外ログメッセージを見るのは、業務共通コンポーネントの開発者ではなく、業務アプリケーションの開発者だからです。業務アプリケーションの開発者が、(内部構造やコードがわからない)業務共通コンポーネントやクラスライブラリから発生した例外ログを見た際に何がトラブルの原因であるのかを正しく把握するためには、業務共通コンポーネントやクラスライブラリから発生した例外ログに詳細な情報が記載されている必要があります。このために、クラスライブラリや業務共通コンポーネント内で自爆処理を行う場合には、詳細でわかりやすいメッセージ情報を含めておく必要があります。(※ この場合であっても、このメッセージ情報を見るのはエンドユーザではなく、そのライブラリを使う開発者です。)

③ null に注意!

自爆コードを記述する場合に気をつけなければならないのが二重障害です。例えば下のコードを見てください。

    1: if (Regex.IsMatch(id, "^[A-Z]{2}[0-9]{4}$") == false) 
    2:     throw new ArgumentException("id : " + id.Length.ToString());

この自爆コードにおいて、id パラメータが null だった場合には、throw new ArgumentException() の処理の中でさらに NullReferenceException 例外が発生してしまうことになります。このようなトラブルを一般的に二重障害と呼ぶのですが、二重障害が発生すると、本来のトラブルがなんだったのかが非常にわかりにくくなります。よって、アプリを組む場合には二重障害を起こさないように注意しながらプログラミングする必要があります。上記のような null を考慮せずに組んだコードは、最も典型的な、二重障害リスクのあるコードとなりますので、十分に注意するようにしてください。

以上でメッセージ情報の使い方に関する説明はおしまいです。次に、例外オブジェクトのネストについて解説しましょう。

[例外オブジェクトのネスト]

.NET や Java の例外オブジェクトには、例外のネストと呼ばれる機能がついており、ある例外オブジェクトが別の例外オブジェクトを内包することができるようになっています。具体的には、すべての例外クラスには .InnerException というプロパティがあり、ここに別の例外オブジェクトを格納することができるようになっいます。

image

例外オブジェクトのネストがどのような場合に使われるのかを示すために、文脈による例外オブジェクトのリスローについて解説します。下図の例を見てください。

image

ASP.NET Web アプリケーションでは、 一般に、ASP.NET ランタイムがまず動作しており、その基盤から我々が記述したユーザアプリケーションが呼び出され、実行されます。このとき、アプリケーションの中で何らかの例外(例えば DivideByZeoException 例外、ゼロ除算例外)が発生したとします。最終的には、発生した例外は、ASP.NET ランタイムの一番の親玉である HttpApplication オブジェクトまで伝えられ、そこに仕掛けられた集約例外ハンドラが動作するのですが、問題なのは、

  • HttpApplication オブジェクトが DivideByZeroException 例外を受け取ったとき、それがユーザアプリケーション内で発生した DivideByZeroException 例外なのか、ASP.NET ランタイム内で発生した DivideByZeroException 例外なのか、判断がつきにくい。

という点です。いや、もちろん例外オブジェクトの中に含まれているスタックトレース情報を頑張って解析すればわからなくもないのですが、これは結構大変です。この問題を解決するため、ASP.NET ランタイムは内部で以下のようなことを行っています。

  • ユーザアプリケーションの処理を行っている最中に、ユーザアプリケーションから未処理例外が通知されたら、これを別の例外(HttpUnhandledException 例外)に取り換えて、上位モジュールに通知する。これにより、上位モジュールである HttpApplication クラスの方では、例外オブジェクトのクラスを見ることで、ユーザアプリケーションで発生した例外なのか、ASP.NET ランタイム内で生じたランタイム障害なのかを区別できるようにしている。
  • しかし、ユーザアプリケーション内部で発生した例外がなんだったのかがわからなくなると困る。このために、HttpUnhandledException 例外オブジェクトの中に、実際に発生した例外オブジェクトをネストさせて保持させ、アプリで発生した元の例外情報が失われないようにしている

ASP.NET ランタイム内部のコードは次のようになっています。(※ 実際の ASP.NET ランタイム内部のコードはもっと複雑なのですが、わかりやすくイメージとして伝えると、次のようなコードが書かれています。)

    1: public class Page
    2: {
    3:   public void ProcessRequestMain()
    4:   {
    5:      try
    6:      {
    7:         // ... 実際にページの処理を行うコード
    8:         // (Page の Init() 処理やイベント処理などを順次進める)
    9:      }
   10:      catch (Exception e)
   11:      {
   12:          throw new System.Web.HttpUnhandledException("Web アプリケーション内で未処理例外が発生しました。", e);
   13:      }
   14:   }
   15: }

このコードの中の肝となる部分は、以下のコードです。

  • throw new System.Web.HttpUnhandledException("Web アプリケーション内で未処理例外が発生しました。", e);

下線部に着目してください。これは、HttpUnhandledException 例外オブジェクトの中に、Web アプリケーション内部で発生した例外オブジェクトをラッピングして持たせておく、というものです。これにより、ASP.NET アプリケーションは元の例外情報を失うことなく、例外情報をうまく上位モジュールに伝えていくことができるようになっています。

このために、ASP.NET ランタイムの集約例外ハンドラで例外を記録した場合には、このネスト例外の情報が記録されていることがしばしばあります。例えば、例外ログに次のような情報が記録されていることがあります。

    1: System.Web.HttpUnhandledException: 種類 System.Web.HttpUnhandledException の例外がスローされました。 
    2: ---> System.DivideByZeroException: 0 で除算しようとしました。
    3:  
    4:    at WebApplication1.WebForm1.Button1_Click(Object sender, EventArgs e) in c:\inetpub\wwwroot\webapplication1\webform1.aspx.cs:line 52
    5:    at System.Web.UI.WebControls.Button.OnClick(EventArgs e)
    6:    at System.Web.UI.WebControls.Button.System.Web.UI.IPostBackEventHandler.RaisePostBackEvent(String eventArgument)
    7:    at System.Web.UI.Page.RaisePostBackEvent(IPostBackEventHandler sourceControl, String eventArgument)
    8:    at System.Web.UI.Page.RaisePostBackEvent(NameValueCollection postData)
    9:    at System.Web.UI.Page.ProcessRequestMain()
   10:    --- 内部例外スタック トレースの終わり ---
   11:    at System.Web.UI.Page.HandleError(Exception e)
   12:    at System.Web.UI.Page.ProcessRequestMain()
   13:    at System.Web.UI.Page.ProcessRequest()
   14:    at System.Web.UI.Page.ProcessRequest(HttpContext context)
   15:    at System.Web.CallHandlerExecutionStep.Execute()
   16:    at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously).
   17:  

この例外ログでは、

  • HttpUnhandledException 例外の中に、(1 行目)
    • DivideByZeroException 例外が含まれていて、(2 行目)
        • DivideByZeroException 例外の中のスタックトレース情報がまず出ていて(3行目~9行目)
    • HttpUnhandledException 例外のスタックトレース情報が出ている(11行目~16 行目)

という形で情報が出ていることがわかります。

ネスト例外がどのような形でイベントログに出力されるのかは、集約例外ハンドラでの例外ログの記録方法次第なのですが、Part.3 で紹介した EMAB (Exception Management Application Block)を利用して例外ログを出力した場合には、もっと読みやすい形でネスト例外ログを出力してくれます。こんな感じ。

    1:  
    2: General Information 
    3: *********************************************
    4: Additional Info:
    5: ExceptionManager.MachineName: NAKAMA07
    6: ExceptionManager.TimeStamp: 2009/01/18 18:48:48
    7: ExceptionManager.FullName: Microsoft.ApplicationBlocks.ExceptionManagement, Version=1.0.3305.33708, Culture=neutral, PublicKeyToken=null
    8: ExceptionManager.AppDomainName: 670e817-12-128767457240733673
    9: ExceptionManager.ThreadIdentity: FAREAST\nakama
   10: ExceptionManager.WindowsIdentity: FAREAST\nakama
   11:  
   12: 1) Exception Information
   13: *********************************************
   14: Exception Type: System.Web.HttpUnhandledException
   15: ErrorCode: -2147467259
   16: Message: 種類 'System.Web.HttpUnhandledException' の例外がスローされました。
   17: Data: System.Collections.ListDictionaryInternal
   18: TargetSite: Boolean HandleError(System.Exception)
   19: HelpLink: NULL
   20: Source: System.Web
   21:  
   22: StackTrace Information
   23: *********************************************
   24:    場所 System.Web.UI.Page.HandleError(Exception e)
   25:    場所 System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
   26:    場所 System.Web.UI.Page.ProcessRequest(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
   27:    場所 System.Web.UI.Page.ProcessRequest()
   28:    場所 System.Web.UI.Page.ProcessRequestWithNoAssert(HttpContext context)
   29:    場所 System.Web.UI.Page.ProcessRequest(HttpContext context)
   30:    場所 ASP.default_aspx.ProcessRequest(HttpContext context)
   31:    場所 System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
   32:    場所 System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)
   33:  
   34: 2) Exception Information
   35: *********************************************
   36: Exception Type: System.Data.SqlClient.SqlException
   37: Errors: System.Data.SqlClient.SqlErrorCollection
   38: Class: 14
   39: LineNumber: 65536
   40: Number: 15350
   41: Procedure: 
   42: Server: \\.\pipe\CA14CFCB-F631-49\tsql\query
   43: State: 1
   44: Source: .Net SqlClient Data Provider
   45: ErrorCode: -2146232060
   46: Message: ファイル C:\Users\nakama\Documents\Visual Studio 2008\Projects\WebSite1\WebSite1\App_Data\pubs.mdf の自動的に名前が付けられたデータベースをアタッチできませんでした。同じ名前のデータベースが既に存在するか、指定されたファイルを開けないか、UNC 共有に配置されています。
   47: Data: System.Collections.ListDictionaryInternal
   48: TargetSite: Void OnError(System.Data.SqlClient.SqlException, Boolean)
   49: HelpLink: NULL
   50:  
   51: StackTrace Information
   52: *********************************************
   53:    場所 System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection)
   54:    場所 System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj)
   55:    場所 System.Data.SqlClient.TdsParser.Run(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj)
   56:    場所 System.Data.SqlClient.SqlInternalConnectionTds.CompleteLogin(Boolean enlistOK)
   57:    場所 System.Data.SqlClient.SqlInternalConnectionTds.AttemptOneLogin(ServerInfo serverInfo, String newPassword, Boolean ignoreSniOpenTimeout, Int64 timerExpire, SqlConnection owningObject)
   58:    場所 System.Data.SqlClient.SqlInternalConnectionTds.LoginNoFailover(String host, String newPassword, Boolean redirectedUserInstance, SqlConnection owningObject, SqlConnectionString connectionOptions, Int64 timerStart)
   59:    場所 System.Data.SqlClient.SqlInternalConnectionTds.OpenLoginEnlist(SqlConnection owningObject, SqlConnectionString connectionOptions, String newPassword, Boolean redirectedUserInstance)
   60:    場所 System.Data.SqlClient.SqlInternalConnectionTds..ctor(DbConnectionPoolIdentity identity, SqlConnectionString connectionOptions, Object providerInfo, String newPassword, SqlConnection owningObject, Boolean redirectedUserInstance)
   61:    場所 System.Data.SqlClient.SqlConnectionFactory.CreateConnection(DbConnectionOptions options, Object poolGroupProviderInfo, DbConnectionPool pool, DbConnection owningConnection)
   62:    場所 System.Data.ProviderBase.DbConnectionFactory.CreatePooledConnection(DbConnection owningConnection, DbConnectionPool pool, DbConnectionOptions options)
   63:    場所 System.Data.ProviderBase.DbConnectionPool.CreateObject(DbConnection owningObject)
   64:    場所 System.Data.ProviderBase.DbConnectionPool.UserCreateRequest(DbConnection owningObject)
   65:    場所 System.Data.ProviderBase.DbConnectionPool.GetConnection(DbConnection owningObject)
   66:    場所 System.Data.ProviderBase.DbConnectionFactory.GetConnection(DbConnection owningConnection)
   67:    場所 System.Data.ProviderBase.DbConnectionClosed.OpenConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory)
   68:    場所 System.Data.SqlClient.SqlConnection.Open()
   69:    場所 PubsDataSetTableAdapters.QueriesTableAdapter.CountAuthorsByState(String state)
   70:    場所 _Default.Button1_Click(Object sender, EventArgs e)
   71:    場所 System.Web.UI.WebControls.Button.OnClick(EventArgs e)
   72:    場所 System.Web.UI.WebControls.Button.RaisePostBackEvent(String eventArgument)
   73:    場所 System.Web.UI.WebControls.Button.System.Web.UI.IPostBackEventHandler.RaisePostBackEvent(String eventArgument)
   74:    場所 System.Web.UI.Page.RaisePostBackEvent(IPostBackEventHandler sourceControl, String eventArgument)
   75:    場所 System.Web.UI.Page.RaisePostBackEvent(NameValueCollection postData)
   76:    場所 System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)

この場合には、12 行目~32 行目が外側の例外オブジェクト(HttpUnhandledException 例外)、34 行目~76 行目が内側の例外オブジェクト(SqlException 例外)になります。例外ログ記録までの全体の処理の流れは以下のようになります。

image

このように、場合によっては例外がネストされた形で報告されることもありますが、ネストされていたとしても正しく例外情報を読めるようになることが重要です。慌てず騒がず、例外ログ情報をまったり読むようにしましょう。

[例外オブジェクトのカスタムプロパティ]

さて最後に、例外オブジェクトのカスタムプロパティについて解説したいと思います。ここまで、例外情報のロギングには EMAB などの部品を使うといいですよ、という解説をしてきたのですが、その理由は単に様々な情報を自動的に追加で記録してくれる、というだけではありません。特に重要ポイントとして、例外オブジェクトのカスタムプロパティのデータを自動的に収集してくれる、という大きなメリットがあります。

これを理解していただくために、SqlException 例外を例に取り上げてみます。SqlException クラスには、Message や StackTrace, InnerException という一般的なプロパティに加えて、SQL Server から通知される様々なエラー情報を格納するためのプロパティを多数持っています。

image

ここで問題になるのが、例外クラスの持つ .ToString() メソッドです。実は、例外オブジェクトの持っている .ToString() メソッドを呼び出しても、内部に持っているすべてのデータを文字列化してくれないことがしばしばあります。

例えば、集約例外ハンドラとして以下のようなコードを書いたとします。

    1: void Application_Error(object sender, EventArgs e) {
    2:     Exception ex = Server.GetLastError(); // 未処理例外を取り出してイベントログに出力
    3:     string errMsg = String.Format("エラーメッセージ\n{0}\n\nスタックトレース\n{1}\n", 
    4:                         ex.Message, ex.StackTrace);
    5:     EventLog.WriteEntry("Application", errMsg, EventLogEntryType.Error);
    6: }

この場合、例外ログにはエラーメッセージ情報やスタックトレース情報は記録されますが、エラー番号(Number プロパティ)や重大度レベル(Class プロパティ)といった、SQL Server が通知してくれる重要な情報がきれいに抜け落ちてしまいます

    1: System.Data.SqlClient.SqlException: DELETE ステートメントは REFERENCE 制約 "FK__titleauth__au_id__0AD2A005" と競合しています。競合が発生したのは、データベース "6C5868E0536A79C8F7BFC6FFAB648A06_OCUMENTS\VISUAL STUDIO 2005\PROJECTS\CONSOLEAPPLICATION1\CONSOLEAPPLICATION1\BIN\DEBUG\PUBS.MDF"、テーブル "dbo.titleauthor", column 'au_id' です。
    2: ステートメントは終了されました。
    3:  
    4:    場所 System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection)
    5:    場所 System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection)
    6:    場所 System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj)
    7:    場所 System.Data.SqlClient.TdsParser.Run(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj)
    8:    場所 System.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString)
    9:    場所 System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean async)
   10:    場所 System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method, DbAsyncResult result)
   11:    場所 System.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(DbAsyncResult result, String methodName, Boolean sendToPipe)
   12:    場所 System.Data.SqlClient.SqlCommand.ExecuteNonQuery()
   13:    場所 ConsoleApplication1.AuthorsDataSetTableAdapters.authorsTableAdapter.DeleteByAuId(String au_id) 場所 C:\Documents and Settings\nakama\My Documents\Visual Studio 2005\Projects\ConsoleApplication1\ConsoleApplication1\AuthorsDataSet.Designer.cs:行 1263
   14:    場所 ConsoleApplication1.Program.DeleteAuthor(String au_id) 場所 C:\Documents and Settings\nakama\My Documents\Visual Studio 2005\Projects\ConsoleApplication1\ConsoleApplication1\Program.cs:行 38
   15:    場所 ConsoleApplication1.Program.Main(String[] args) 場所 C:\Documents and Settings\nakama\My Documents\Visual Studio 2005\Projects\ConsoleApplication1\ConsoleApplication1\Program.cs:行 16

この例では、幸いにしてメッセージだけでエラー情報が解析できましたが、Number プロパティなどが欠けていると十分な障害解析ができないわけです。ところが EMAB を使った場合には、こうしたカスタムプロパティを自動的に解析し、これらの情報もロギングしてくれます。同じ例外を EMAB でロギングした場合には、以下のようになります。

    1: General Information 
    2: *********************************************
    3: Additional Info:
    4: ExceptionManager.MachineName: NAKAMA26
    5: ExceptionManager.TimeStamp: 2005/11/28 17:56:19
    6: ExceptionManager.FullName: Microsoft.ApplicationBlocks.ExceptionManagement, Version=1.0.1815.31615, Culture=neutral, PublicKeyToken=null
    7: ExceptionManager.AppDomainName: ConsoleApplication1.exe
    8: ExceptionManager.ThreadIdentity: 
    9: ExceptionManager.WindowsIdentity: FAREAST\nakama
   10:  
   11: 1) Exception Information
   12: *********************************************
   13: Exception Type: System.Data.SqlClient.SqlException
   14: Errors: System.Data.SqlClient.SqlErrorCollection
   15: Class: 16
   16: LineNumber: 1
   17: Number: 547
   18: Procedure: 
   19: Server: \\.\pipe\2A2A18A2-5F95-46\tsql\query
   20: State: 0
   21: Source: .Net SqlClient Data Provider
   22: ErrorCode: -2146232060
   23: Message: DELETE ステートメントは REFERENCE 制約 "FK__titleauth__au_id__0AD2A005" と競合しています。競合が発生したのは、データベース "6C5868E0536A79C8F7BFC6FFAB648A06_OCUMENTS\VISUAL STUDIO 2005\PROJECTS\CONSOLEAPPLICATION1\CONSOLEAPPLICATION1\BIN\DEBUG\PUBS.MDF"、テーブル "dbo.titleauthor", column 'au_id' です。
   24: ステートメントは終了されました。
   25: Data: System.Collections.ListDictionaryInternal
   26: TargetSite: Void OnError(System.Data.SqlClient.SqlException, Boolean)
   27: HelpLink: NULL
   28:  
   29: StackTrace Information
   30: *********************************************
   31:    場所 System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection)
   32:    場所 System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection)
   33:    場所 System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj)
   34:    場所 System.Data.SqlClient.TdsParser.Run(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj)
   35:    場所 System.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString)
   36:    場所 System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean async)
   37:    場所 System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method, DbAsyncResult result)
   38:    場所 System.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(DbAsyncResult result, String methodName, Boolean sendToPipe)
   39:    場所 System.Data.SqlClient.SqlCommand.ExecuteNonQuery()
   40:    場所 ConsoleApplication1.AuthorsDataSetTableAdapters.authorsTableAdapter.DeleteByAuId(String au_id) 場所 C:\Documents and Settings\nakama\My Documents\Visual Studio 2005\Projects\ConsoleApplication1\ConsoleApplication1\AuthorsDataSet.Designer.cs:行 1263
   41:    場所 ConsoleApplication1.Program.DeleteAuthor(String au_id) 場所 C:\Documents and Settings\nakama\My Documents\Visual Studio 2005\Projects\ConsoleApplication1\ConsoleApplication1\Program.cs:行 38
   42:    場所 ConsoleApplication1.Program.Main(String[] args) 場所 C:\Documents and Settings\nakama\My Documents\Visual Studio 2005\Projects\ConsoleApplication1\ConsoleApplication1\Program.cs:行 16

エラー情報の中の、14~22 行目に着目してください。これらの情報は、SqlException 例外オブジェクトが持っている非常に重要な情報ですが、EMAB であればこれらについてもきちんとロギングしてくれる、というわけです。

こうした、例外オブジェクトのカスタムプロパティが持っている情報というのは障害解析に非常に有益な情報が多く、これらが適切にロギングされていると障害解析はかなりラクになります。こうした観点からも、ぜひ EMAB などの例外出力モジュールを活用するようにしてください。

[まとめ]

というわけで、全 4 回+αにわたって .NET の例外処理についていろんな解説をしてきましたが(All About .NET 例外とか名前つけたい....(笑))、全体を振り返って要点をまとめると、以下のようになります。

  • まず、アプリケーションコードとして正しい例外処理コードを書くことが重要。

    そのためには、業務エラーとアプリケーション/システムエラーをきちんと区別し、例外をアプリケーション/システムエラーのときだけに使うようにする必要がある。

  • むやみに try-catch や throw コードを書かない。

    業務フローチャートを意識し、必要な場所に絞ってこれらのコードを書く。基本は「書かない」。

  • 集約例外ハンドラを使って、正しく例外ログを取る。

    EMAB などの部品を使ってログを出力するようにすると便利。

  • 出力された例外ログを正しく読めるようになることが重要。

    例外オブジェクトに含まれる情報のうち、特に例外クラスそのもの、メッセージ情報、スタックトレース情報の 3 つが重要。これらを正しく読めるようになるだけで、障害解析やアプリケーションデバッグは遥かに容易になる。

.NET や Java のアプリケーションにおいて、障害解析を困難にしている代表的な問題の一つが、正しく例外が取り扱われていないことなのですが、例外を正しく使えるようになることは、アプリケーション開発者にとって避けて通れないキーポイントです。ぜひここまでの 4 回のエントリを活用して、正しい例外処理コードを記述するようにしてください。