こんにちは!
今日は 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; で “別の場所” から投げ直すと、ランタイムは
- 例外を キャッチしたメソッド を新しいスタックの起点にする
- ここより下の呼び出し履歴をまるっと捨てる
という振る舞いを見せます。
結果、ログを眺めても
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
