C# で扱う例外は、基本的に System.Exception を頂点とする階層で表現されます。
日常的には ArgumentException や InvalidOperationException などの派生型を使いますが、基底である System.Exception を正しく理解しておくと、例外処理の設計が一段安定します。
この記事では System.Exception の役割、主要プロパティ・メソッド、例外オブジェクトから得られる情報、実務上の注意点をまとめます。
まず結論: System.Exception とは
System.Exception は、.NET の例外を表す基底クラスです。
- 例外メッセージ
- 例外の種類(型)
- 発生箇所(スタックトレース)
- 内部例外(原因となった例外)
- 補足情報(
Data)
といった、障害解析に必要な情報を一つのオブジェクトに保持します。
C# 言語の throw で投げる値は System.Exception 派生である必要があります。
「すべての例外は Exception 派生か?」への補足
通常の C# 開発では「はい」と考えて問題ありません。
- C# では
throwできるのはSystem.Exception派生オブジェクト catchもException階層を前提に設計される- .NET ライブラリもこの前提で例外型を提供する
実務上は「例外 = Exception 派生」として扱うのが正しい運用です。
System.Exception の主なコンストラクタ
Exception には代表的に次のコンストラクタがあります。
// 1) メッセージなし
throw new Exception();
// 2) メッセージ付き
throw new Exception("想定外の状態です。");
// 3) メッセージ + 内部例外
try
{
// 低レイヤー処理
}
catch (Exception ex)
{
throw new Exception("データ取得に失敗しました。", ex);
}
3番目の内部例外付きコンストラクタは非常に重要です。 高レイヤー向けに文脈を追加しつつ、元の原因情報を失わずに伝播できます。
主要プロパティ
System.Exception で特に重要なプロパティを優先順で見ます。
Message
人間向けの説明文です。ログや UI 表示の最小単位になります。
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
StackTrace
どの呼び出し経路で例外が発生したかを示す文字列です。 デバッグでは最重要情報の一つです。
catch (Exception ex)
{
Console.WriteLine(ex.StackTrace);
}
InnerException
この例外を引き起こした元例外への参照です。 「原因の連鎖」をたどるために使います。
catch (Exception ex)
{
Exception? current = ex;
while (current is not null)
{
Console.WriteLine($"[{current.GetType().Name}] {current.Message}");
current = current.InnerException;
}
}
Source
例外をスローしたアセンブリ名などのソース情報です。
TargetSite
例外が発生したメソッド(MethodBase)を示します。
Data
IDictionary 形式の追加情報です。
障害調査用のキー/値を載せるのに使えます。
var ex = new Exception("注文処理に失敗しました。");
ex.Data["OrderId"] = orderId;
ex.Data["UserId"] = userId;
throw ex;
Data は便利ですが、機密情報を入れすぎないよう注意が必要です。
HelpLink
補足ドキュメントへのリンクを設定できます。
HResult
例外に対応する数値コードです。COM 連携や低レイヤー解析で使われます。
主要メソッド
ToString()
例外の型名、Message、StackTrace、InnerException 連鎖などをまとめた文字列を返します。
実務では Message 単体より ex.ToString() をログに残す方が有用です。
catch (Exception ex)
{
logger.LogError(ex.ToString());
}
GetBaseException()
内部例外チェーンの最深部(根本原因)を取得します。
catch (Exception ex)
{
Exception root = ex.GetBaseException();
Console.WriteLine($"Root cause: {root.GetType().Name} - {root.Message}");
}
例外オブジェクトから得られる情報
実際の運用では、次の観点で情報を取り出すと調査効率が上がります。
- 種類:
ex.GetType().FullName - 要約:
ex.Message - 発生箇所:
ex.StackTrace - 原因:
ex.InnerException/ex.GetBaseException() - 文脈:
ex.Data
最小限のログ例:
catch (Exception ex)
{
logger.LogError(
"Type={Type} Message={Message} Base={Base} Trace={Trace}",
ex.GetType().FullName,
ex.Message,
ex.GetBaseException().Message,
ex.StackTrace);
}
使用上の注意点(重要)
1. Exception を安易に throw しない
アプリコードで throw new Exception(...) を多用すると、呼び出し側が種類ごとの対処をしづらくなります。
- 引数不正:
ArgumentException/ArgumentNullException/ArgumentOutOfRangeException - 状態不正:
InvalidOperationException - 未実装:
NotImplementedException
のように、意味が伝わる派生例外を優先します。
2. catch (Exception) の丸飲みを避ける
catch (Exception) 自体は悪ではありませんが、握りつぶしは危険です。
try
{
DoWork();
}
catch (Exception ex)
{
// 最低限ログを残して再スローする
logger.LogError(ex, "DoWork failed");
throw;
}
3. 再スローは throw; を使う
throw ex; はスタックトレースを潰します。
再スローは必ず throw; を使います。
catch (Exception)
{
throw; // 元のスタックトレースを保持
}
4. 例外を制御フローに使いすぎない
通常分岐で扱える条件は if や TryParse 系で処理し、
本当に異常なケースだけを例外にします。
5. カスタム例外は本当に必要なときだけ
ドメイン固有の意味を持たせたいときはカスタム例外が有効です。
public sealed class PaymentFailedException : Exception
{
public string TransactionId { get; }
public PaymentFailedException(string transactionId, string message, Exception? inner = null)
: base(message, inner)
{
TransactionId = transactionId;
}
}
用途が曖昧なら既存の標準例外を使う方が保守しやすいです。
SystemException と ApplicationException について
歴史的に SystemException や ApplicationException という型がありますが、
現在は以下の理解で十分です。
SystemException: CLR や BCL 系の基盤例外の系統ApplicationException: 現代の C# では積極利用が推奨されない
独自例外を作る場合、通常は Exception 直接継承で問題ありません。
まとめ
System.Exceptionは .NET 例外モデルの中核で、診断に必要な情報を保持する- 特に
Message、StackTrace、InnerException、Dataが実務で重要 - ログは
MessageだけでなくToString()や根本原因情報まで残す - 再スローは
throw;、握りつぶしは避ける throw new Exception(...)の多用より、意味に合った派生例外を選ぶ
System.Exception を正しく使うだけで、障害時の復旧スピードとコードの保守性は大きく改善します。