InnerExceptionは誰が設定するのか(例外が再スローされるときにいつでも設定される訳ではない)

MSDNにはInnerExceptionについて以下のような説明があります。

ファイルを読み取り、そのファイルのデータの書式を指定する関数があるとします。この例では、ファイルの読み取りを試みるコードとして、IOException がスローされます。この関数は、IOException を受け取り、FileNotFoundException をスローします。IOException を FileNotFoundException の InnerException プロパティに保存でき、それにより、初期エラーの原因を検査するための FileNotFoundException を受け取るコードが有効になります。


FileNotFoundExceptionが発生するとき、InnerExceptionにはIOExceptionが設定されるんだと思いますが実際には違います。以下のようなコードで存在しないファイルを読み込むとFileIOExceptionは発生しますが、InnerExceptionはNullになります。



System.IO.File.ReadAllText(@"notfound.txt");


MSDNの説明はあくまで例えとしての話であり、ある例外を別の例外に変換して再スローする場合に必ずInnerExceptionが設定されるわけではありません。以下のような明示的に再スローする処理を書いてもexにはInnerExceptionは設定されません。nullの状態です。



try
{
this.Call2();
}
catch (Exception ex)
{
throw ex;
}


つまり、InnerExceptionは明示的に例外を設定するコードを書かない限りは設定されません。


InnerExceptionが発生する例としてTargetInvocationExceptionがあります。名前は聞きなれないかもしれませんが、よく見かける"呼び出しのターゲットが例外をスローしました。"という例外です。リフレクションによってメソッドを呼び出すときにメソッドがスローする例外をCLRはすべてTargetInvocationExceptionに変換し、もとの例外をInnerExceptionに設定します。つまりゼロ除算であってもスタックオーバーフローであっても、NullReferenceExeptionであってもCLRはTargetInvocationExceptionに変換します。これにより、例外が発生した箇所は変換されリフレクションによってメソッドを呼び出した箇所となります。(実際に発生した場所ではなく)


例外の変換は本来のエラーが分からなくなってしまうので、あまり良い方法とは思えません。本来のエラーに再変換するには以下のようなコードを使います。



try
{
test t = new test();

var mi = t.GetType().GetMethod("doWork");
mi.Invoke(t, null);

}
catch (System.Reflection.TargetInvocationException ex)
{
throw ex.InnerException;
}


このコードは、変換された例外を、逆変換して元の例外に戻しています。全体で見ると無駄な処理ですね。


★★★★★★★★★★★★

  • まとめ

InnerExceptionは自動的に設定されない。例外を変換する処理で元の例外をInnerExceptionに設定する必要がある。また、例外を変換すること自体が必要かどうか検討する必要がある。既定例外のハンドラではInnerExceptionもログに出力するべき。