大規模なアプリケーション開発、特に多層アーキテクチャ(Web API、ビジネスロジック、データアクセス層など)を採用しているシステムでは、例外が「ラップ(包み込み)」されて上位層に伝播することが一般的です。
例えば、データベース接続エラーをデータアクセス層でキャッチし、業務例外として投げ直すようなケースです。この場合、最上位でキャッチした例外のMessageだけを見ても、「業務エラーが発生した」ことしか分からず、根本原因(Root Cause)であるSQLエラーの内容が見えないことがあります。
C#のInnerExceptionプロパティを再帰的にたどることで、ラップされた全ての例外をフラットなリストとして取得し、エラーの全容をログに記録する方法を解説します。
InnerExceptionの階層構造と課題
通常、例外は以下のようにチェーン状に繋がっています。
- 最上位の例外: コントローラー層で捕捉される(例:
ApplicationException) - 中間の例外: サービス層でラップされたもの(例:
ServiceException) - 根本の例外: 実際に発生したエラー(例:
SqlExceptionやNullReferenceException)
これらをすべてログに出力するには、InnerExceptionがnullになるまでループまたは再帰処理を行う必要があります。C#のイテレータ(yield return)を使用すると、この処理を非常にエレガントな拡張メソッドとして実装できます。
実践的なコード例:例外チェーンのフラット化
以下のコードは、支払処理システムにおいて多重にラップされた例外が発生した状況をシミュレートし、拡張メソッドを使って全ての例外情報を列挙する例です。
using System;
using System.Collections.Generic;
using System.IO;
namespace ErrorHandling
{
class Program
{
static void Main()
{
try
{
// 多重に例外がラップされる処理を実行
ProcessPaymentTransaction();
}
catch (Exception ex)
{
Console.WriteLine("=== エラー解析開始 ===");
// 拡張メソッドを使用して、例外の階層をフラットなリストとして取得
foreach (var innerEx in ex.GetInnerExceptions())
{
Console.WriteLine($"Type: {innerEx.GetType().Name}");
Console.WriteLine($"Message: {innerEx.Message}");
Console.WriteLine("-------------------------");
}
}
}
// 3階層の例外をシミュレートするメソッド
static void ProcessPaymentTransaction()
{
try
{
CallPaymentApi();
}
catch (Exception ex)
{
// 3. 最上位: 業務ロジックのエラーとしてラップ
throw new InvalidOperationException("支払トランザクションの処理に失敗しました。", ex);
}
}
static void CallPaymentApi()
{
try
{
ReadCertificateFile();
}
catch (Exception ex)
{
// 2. 中間: 通信/IOエラーとしてラップ
throw new IOException("外部APIへの接続準備中にエラーが発生しました。", ex);
}
}
static void ReadCertificateFile()
{
// 1. 根本原因: ファイルが見つからない
throw new FileNotFoundException("証明書ファイル(cert.pfx)が見つかりません。");
}
}
// 例外処理用の拡張メソッド定義
public static class ExceptionExtensions
{
/// <summary>
/// 自身を含む全てのInnerExceptionを再帰的に列挙します。
/// </summary>
public static IEnumerable<Exception> GetInnerExceptions(this Exception ex)
{
if (ex == null)
{
yield break;
}
// まず自分自身を返す
Exception current = ex;
while (current != null)
{
yield return current;
// 次の内部例外へ移動
current = current.InnerException;
}
}
}
}
実行結果
=== エラー解析開始 ===
Type: InvalidOperationException
Message: 支払トランザクションの処理に失敗しました。
-------------------------
Type: IOException
Message: 外部APIへの接続準備中にエラーが発生しました。
-------------------------
Type: FileNotFoundException
Message: 証明書ファイル(cert.pfx)が見つかりません。
-------------------------
技術的なポイント
1. イテレータ (yield return) の活用
入力された題材では再帰呼び出しを使用していましたが、InnerExceptionは通常「単方向リスト」のような構造であるため、whileループを使用したイテレータの方がスタック消費が少なく、効率的です。yield returnを使用することで、呼び出し側はforeachで即座に列挙を開始できます。
2. 拡張メソッドによる再利用性
ExceptionExtensionsクラスとして定義し、this Exception exとすることで、あらゆる例外オブジェクトに対して .GetInnerExceptions() メソッドが呼べるようになります。これにより、ログ出力ロジックを共通化しやすくなります。
3. AggregateExceptionへの対応(注意点)
上記のコードは標準的な例外チェーン(InnerExceptionが1つ)を想定しています。しかし、TaskやParallel処理で使用されるAggregateExceptionの場合、内部に複数の例外リスト(InnerExceptionsプロパティ)を持っています。
もし非同期処理を多用する環境であれば、以下のように分岐処理を加えることで、より堅牢な実装になります。
public static IEnumerable<Exception> GetAllExceptions(this Exception ex)
{
if (ex == null) yield break;
// 自分自身を返す
yield return ex;
// AggregateExceptionの場合は、内部の複数の例外を処理
if (ex is AggregateException aggEx)
{
foreach (var inner in aggEx.InnerExceptions)
{
// 再帰的に取得(ここは木構造になるため再帰が適切)
foreach (var child in inner.GetAllExceptions())
{
yield return child;
}
}
}
// 通常の例外でInnerExceptionがある場合
else if (ex.InnerException != null)
{
// 次の階層へ(再帰またはループ)
foreach (var child in ex.InnerException.GetAllExceptions())
{
yield return child;
}
}
}
まとめ
例外のInnerExceptionをたどる処理は、トラブルシューティングの質を大きく左右します。エラーログには「何が起きたか(最上位)」だけでなく、「なぜ起きたか(最下位)」の情報が不可欠です。今回紹介した拡張メソッドを共通ライブラリに組み込み、常に根本原因までトレースできる環境を整えることを推奨します。
