こんにちは!
今日は 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