[C#,VB.NET]警告CA2200の意味

こんにちは!
今日は C# のコード品質アナライザーが発する CA2200「スタックトレースを保持して再スローせよ」 という警告について、じっくり語りたいと思います。


再現環境

  • Visual Studio 2022
  • .NET9

そもそも CA2200 って何なの?

Visual Studio や dotnet build には膨大な Roslyn アナライザーが組み込まれています。その 1 つが CA2200: Rethrow to preserve stack details です。名前のとおり「例外を再スローするならスタックトレースを壊さないようにね」というお節介ルールなのですが、これが意外と盲点になります。

  • 検出条件: catch (Exception ex) { throw ex; } のように 例外変数を指定して 再スローしている
  • なぜダメ?: throw ex;ex.StackTrace をリセットし、「例外が最初に起きた場所」が失われる
  • いつから?: .NET 5 以降は標準で有効。無視してもコンパイルは通りますが、警告が残ります Microsoft Learn

「throw ex;」が犯している罪

例外オブジェクトは、生成された瞬間に StackTrace を内部にキャプチャします。
ところが throw ex; で “別の場所” から投げ直すと、ランタイムは

  1. 例外を キャッチしたメソッド を新しいスタックの起点にする
  2. ここより下の呼び出し履歴をまるっと捨てる

という振る舞いを見せます。
結果、ログを眺めても

Unhandled exception in Foo.Bar()
   at Program.Main()

といった短い情報しか残らず、「いったい Foo.Bar() の中のどこで NullReference が出たの!?」と悩むハメになります。

マイクロソフトの公式ドキュメントでも「例外を再スローするなら throw; を使え」と明言されています Microsoft Learn


正しい再スロー 〜最短コースは throw; 一択〜

基本形

try
{
    Dangerous();   // ここで例外が出るかも
}
catch (Exception ex)
{
    Log(ex);       // ここでログだけ書く
    throw;         // ← 例外名を付けない
}

throw; はキャッチした例外オブジェクトをそのまま上位へ伝搬し、StackTrace を一切いじりません
デバッグ時に コールスタック を開くと、Dangerous() 内の該当行まできれいに追えます。

ログを書きつつ伝搬したいパターン

「例外を握りつぶさず、とりあえずログは残したい」というニーズは多いです。
その場合も throw; → ログは catch 内で完結 が鉄板です。


例外をラッピングしたいケース

とはいえ「ライブラリ内部の詳細を外に漏らしたくない」「追加の文脈を付けて投げたい」という場面もあります。

try
{
    LoadConfig();
}
catch (Exception ex)
{
    throw new ConfigLoadException("設定ファイルの読み込みに失敗しました", ex);
}
  • 新しい独自例外を生成
  • ex を必ず innerException に渡す

こうすれば外側の ConfigLoadException には新しいスタックトレースが記録され、InnerException の中に 元のスタック が丸ごと残ります。デバッグで2段掘れば出発点を確認できます。


それでも困ったら ExceptionDispatchInfo

よりトリッキーなシナリオ──たとえば 今は処理を続行し、あとで同じ例外を再スローしたい ──では ExceptionDispatchInfo が奥の手になります。

try
{
    Dangerous();
}
catch (Exception ex)
{
    var edi = ExceptionDispatchInfo.Capture(ex);
    // 何らかの非同期処理……
    edi.Throw(); // スタックトレースを完全保持したまま再スロー
}

.Throw() は内部でスタック情報を復元してくれるため、Dangerous() の発火位置が失われません Microsoft Learn
とはいえ日常的に登場する API ではないので、まずは throw; が最適解 と覚えておけば十分です。


よくある疑問&誤解

Q1. throw ex;throw; の差、昔の .NET でも同じ?

はい、.NET Framework 1.0 の頃から一貫した仕様です。
CA2200 が「比較的新しい警告」に見えるのは Roslyn アナライザーへの統合が最近だからで、本質は昔から変わりません。

Q2. throw; したらラムダ内や async/await でもトレースは残る?

await を挟むと論理スタックが分断されるため、完全な同期呼び出しほどきれいには残りません
それでも throw ex; よりは格段に有用なので、非同期でもまず throw; を選びましょう。

Q3. プロジェクト全体で CA2200 を無効化していい?

.editorconfig

dotnet_diagnostic.CA2200.severity = none

と書けば静かになりますが、デバッグで泣く未来が待っています。
「仕方なく無視した」ではなく、なぜ無視するのか・ログ戦略はどうするかをチームで合意してから設定しましょう。


まとめ 〜「throw;」を手癖にしよう〜

例外は情報の宝庫。宝を捨ててはいけません。

  • CA2200 が警告してくれるのは「宝を捨てる書き方」をしたとき
  • throw; ならスタックトレース温存、デバッグ幸福
  • ラッピング するなら new XxxException(..., ex)InnerException を残す
  • 奥の手 が欲しければ ExceptionDispatchInfo

たった 1 文字減らすだけで、未来の自分とチームメイトがバグ解析にかける時間を何十倍も節約できます。今夜のコミットからぜひ “throw だけ” のスタイルを取り入れてみてください。

というわけで、今日は CA2200 と正しい例外再スローの話でした。この記事が「謎の throw ex; を grep して置換だ!」という小さな改善のきっかけになればうれしいです。ではまた!

参考資料

CA2200: スタック詳細を保持するために再度スローします (コード分析) – .NET | Microsoft Learn