【C#】一度catchした例外を再度throwするときに気を付けること

C#で例外をキャッチした際に、再度別の場所で投げるケースがあると思います。正しくコーディングしないとスタックトレースが途切れてしまい、デバッグに困るので気を付ける点と対処法をまとめました。

通常の再スロー

catch文の中で再度throwする場合はこのような形になると思います。

Example.cs
using System;

public class Example
{
    private static void NormalRethrow()
    {
        try
        {
            MethodA();
        }
        catch (Exception e)
        {
            throw;
        }
    }

    private static void MethodA() => MethodB();
    private static void MethodB() => throw new Exception();
}

これを実行すると、以下の様なスタックトレースの例外がスローされます。

この例外は、最初にこの呼び出し履歴 
    Example.MethodB() 場所: Example.cs
    Example.MethodA() 場所: Example.cs
    Example.MyTestMethod() 場所: Example.cs でスローされました

ちゃんと途中のスタックトレースも出力されていますね。

失敗するケース

問題はここからで以下の様にcatchで取得した例外を変数eで宣言した際、throw e;とすると想定外のスタックトレースになってしまいます。

Example.cs
using System;

public class Example
{
    private static void NormalRethrow()
    {
        try
        {
            MethodA();
        }
        catch (Exception e)
        {
            throw e; // ここでeを投げる
        }
    }

    private static void MethodA() => MethodB();
    private static void MethodB() => throw new Exception();
}

実行するとこのようなスタックトレースになります。

Example.MyTestMethod() (Example.cs):行 29

途中の履歴が破棄されてしまいました。これは「ここでeという例外が発生した」という認識がされてしまうからです。今回はcatch文の中で投げていますが、外で投げるケースもあります。その場合先ほどの投げ方は出来ないので、方法を変える必要があります。

方法1:別の例外の中に含める

InnerExceptionとして別の例外に入れてしまおうという考え方ですね。以下のコードで試してみましょう。ここでは例としてArgumentExceptionを使用してします。

Example.cs
using System;

public class Example
{
    private static void NormalRethrow()
    {
        try
        {
            MethodA();
        }
        catch (Exception e)
        {
            throw new ArgumentException(null, e); // ここでeをArgumentExceptionに渡して投げる
        }
    }

    private static void MethodA() => MethodB();
    private static void MethodB() => throw new Exception();
}

実行すると以下の様なスタックトレースになります。

   場所 Example.MethodB() (Example.cs):行 33
   場所 Example.MethodA() (Example.cs):行 32
   場所 Example.MyTestMethod() (Example.cs):行 24

途中の履歴もしっかりスタックトレースに出力されていますね。しかし、これでは元の例外から変わってしまうので、変えたくないケースというのも出てきます。

方法2:ExceptionDispatchInfoを使用して再スローする

ExceptionDispatchInfoクラスにはCature()メソッドがあります。(ドキュメント
このメソッドは「現在の位置から」ではなく、「例外が発生した位置から」に上書きして再スローすることができるExceptionDispatchInfoのオブジェクトを返します。そのオブジェクトに対しThrow()メソッドを実行することで、再スローを再現することができます。

以下のコードで確認してみましょう。

Example.cs
using System;

public class Example
{
    private static void NormalRethrow()
    {
        Exception ex = null;

        try
        {
            MethodA();
        }
        catch (Exception e)
        {
            ex = e; // 変数に格納
        }

        ExceptionDispatchInfo.Capture(ex).Throw(); // ExceptionDispatchInfoを利用して再度スロー
    }

    private static void MethodA() => MethodB();
    private static void MethodB() => throw new Exception();
}

このコードを実行するとこのようなスタックトレースになります。

   場所 Example.MethodB() (Example.cs):行 38
   場所 Example.MethodA() (Example.cs):行 37
   場所 Example.MyTestMethod() (Example.cs):行 27
--- 直前の場所からのスタック トレースの終わり ---
   場所 Example.MyTestMethod() (Example.cs):行 34

このようにExceptionDispatchInfo.Capture(ex)を実行した行も履歴として追加されたスタックトレースになりました。

まとめ

いかがでしょうか。あくまでこれらの方法は一例に過ぎずほかの方法もあるかもしれません。それぞれの方法にメリットデメリットがあるので、皆さんのケースに合わせて選択してください。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です