はじめに
毎日元気にC#の実装中、最近あまり見かけなかった例外処理を実装することになりました。
そこで、throw
とthrow ex
がどう違うのか、忘れてしまったので、備忘録ついでなので、今回この記事でまとめておくことにしました。
それぞれの特徴
さっそく、それぞれの特徴をまとめてみましょう。
throw
: 元の例外情報を保持
throw ex
: 例外情報がリセットされる
ざっくりわけるとこんな感じです。
ソースコードをもとに、見ていきましょう。
最初に、例外処理を比較するための実装を用意しておきます。
public class ExceptionDemo
{
public void MethodA()
{
MethodB(); // 15行目
}
public void MethodB()
{
MethodC(); // 20行目
}
public void MethodC()
{
throw new InvalidOperationException("何かエラーが発生"); // 25行目 - 実際のエラー発生箇所
}
}
パターン1: throw を使った場合
public void BadExceptionHandler()
{
try
{
var demo = new ExceptionDemo();
demo.MethodA();
}
catch (Exception ex)
{
Logger.Error($"エラーが発生: {ex.Message}");
throw;
}
}
// スタックトレース結果
InvalidOperationException: // 何かエラーが発生
at ExceptionDemo.MethodC() in Program.cs:line 25
at ExceptionDemo.MethodB() in Program.cs:line 20
at ExceptionDemo.MethodA() in Program.cs:line 15
at Program.BadExceptionHandler() in Program.cs:line 35
御覧の通りthrow
を使用している場合、どこでなんのエラーが発生したか結果で一目で確認することができます。
パターン2: throw ex を使った場合
では次に、'throw ex'を使用している例を見てみましょう。
public void GoodExceptionHandler()
{
try
{
var demo = new ExceptionDemo();
demo.MethodA(); // 35行目
}
catch (Exception ex)
{
// ログ出力など
Logger.Error($"エラーが発生: {ex.Message}");
throw ex; // 例外を再作成して投げ直し
}
}
// スタックトレース結果
InvalidOperationException: // 何かエラーが発生
at Program.GoodExceptionHandler() in Program.cs:line 47
一方こちらでは、どこで何のエラーが発生したかが一目でわかりません。
これだと、折角例外処理を書いているのにエラー原因を特定するのにだいぶ苦労します(経験談)。
なぜこの違いが生まれるのか
この違いが生まれる理由について、少し掘り下げてみましょう。
throwの動作
throw
は現在の例外オブジェクトをそのまま再スローします。
これによって、元の例外が発生した位置のスタックトレース情報が保持されるという特徴があります。
つまり、例外を最初にキャッチした場所ではなく、実際にエラーが発生した箇所の情報をデバッグ時に参照することができます。
throw exの動作
一方のthrow ex;
は、ぱっと見ex
を再スローしているようにも見えますが、実際には新たに例外を投げなおしているというふうに、システム上みなされてしまいます。
その結果、スタックトレース情報はリセットされ、throw ex; を記述した位置が新しい例外発生地点として記録されます。
throw exのいい感じの使い方
ここまでくると、業務上での実装でthrow ex
の良いところなくないですかとなるので、throw ex
のより良い使い方も考えたいと思います。
最も簡易的な例では、例外処理に追加の情報を付与したい場合です。
catch (Exception ex)
{
// 方法1: InnerExceptionとして元の例外を保持
throw new CustomException("追加のエラー情報", ex);
// 方法2: 元の例外にデータを追加
ex.Data["AdditionalInfo"] = "追加情報";
throw;
}
本番運用では具体的なエラー出力がバグ発見の大きな要因になるので、InnerException
などで具体的な情報を付与することができるのは大きな利点です。
まとめ
ここまでの話をまとめると、基本的に実装するうえでは、throw
を使うことが推奨されます。追加情報などを付与したい場合などは、throw ex
も活用できますが、PoC段階の機能実装レベルでは、throw
で十分な感じがします。
ちなみにこの話は.netの設計思想にも反映されています。
.netの設計として、例外処理は通常フローとは異なる稀な状態と定義していて、例外処理を「手元で処理されるべきものではなく、むしろ「稀な事態(= exceptional)」として扱われ、一部中央集権的に管理すべき」 と設計チームの一人は唱えています。
設計思想まで深堀は今記事のスコープ外になりますが、実装中の違和感が、C#/.netがどれくらい堅牢に設計されているか、感じさせられますね。
参考リンク
・Microsoft Docs: throw ステートメント
・Best Practices for Exception Handling
・設計者_Hejlsbergのインタビュー