C# Tips | ログ・例外・診断:例外再スロー

C# C#
スポンサーリンク

はじめに:「例外再スロー」は“いったん受け止めてから、ちゃんと投げ直す”テクニック

例外をキャッチしたあとに、こう思う場面がよくあります。

ログは取りたい
でも、この場では処理できない
だから、上の層に判断を任せたい

このときに使うのが 例外再スロー(rethrow) です。
ただし、書き方を間違えると「どこで本当に落ちたのか」という情報が消えてしまいます。

ここでは、初心者向けに

なぜ再スローが必要なのか
throw;throw ex; の違い
ログを取ってから再スローする正しいパターン
再スローと「例外ラップ」との違い

を、例題を交えて丁寧に解説します。


例外再スローとは何か

「ここでは処理しないけど、素通りもさせない」ための仕組み

まず、例外再スローのイメージから。

例外が発生する
いったん catch で受け止める
ログを書いたり、何か補助的な処理をする
そのあと、同じ例外を上の層に投げ直す

これが「再スロー」です。

コードで書くと、こういう形です。

try
{
    DangerousOperation();
}
catch (Exception ex)
{
    Log(ex);   // ログを取る
    throw;     // もう一度投げる(再スロー)
}
C#

ここでの重要ポイントは、「再スローは“例外を握りつぶさないための礼儀”みたいなもの」という感覚です。
ここでは完全には対処できないから、上の層にも知らせる、という姿勢です。


一番大事なポイント:throw; と throw ex; の違い

これを知らないと、スタックトレースが壊れる

C# には、例外を投げる書き方が大きく 2 つあります。

throw;
throw ex;

見た目は似ていますが、意味はまったく違います。

throw; は「そのまま投げ直す」

throw; は、「今キャッチしている例外を、そのまま上に投げ直す」という意味です。
このとき、スタックトレース(どのメソッドを通ってここに来たかの履歴)は 元のまま 保たれます。

catch (Exception ex)
{
    Log(ex);
    throw;  // スタックトレースは元のまま
}
C#

throw ex; は「新しく投げ直す」

一方、throw ex; は、「ex を“新しく投げる”」という扱いになります。
その結果、スタックトレースの起点が「この throw ex; の行」に書き換わってしまいます。

catch (Exception ex)
{
    Log(ex);
    throw ex;  // スタックトレースがここからに書き換わる
}
C#

調査するときにスタックトレースを見ると、

本当はもっと下のメソッドで落ちているのに
「この catch の場所で落ちた」ように見えてしまう

という悲しい状態になります。

ここでの超重要ポイントは、
「同じ例外を再スローしたいときは、必ず throw; を使う。throw ex; は使わない」
ということです。


ログを取ってから再スローする、よくある実務パターン

「ここでログ」「最終的なハンドリングは上の層」

実務で一番よく出てくるのが、「ログだけ取って、処理は上に任せる」パターンです。

例えば、サービス層でこう書きます。

public void ProcessOrder(int orderId)
{
    try
    {
        _orderRepository.Save(orderId);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "注文処理中にエラーが発生しました。OrderId={OrderId}", orderId);
        throw;  // ここでは握りつぶさず、上に投げる
    }
}
C#

UI 層では、最終的なエラーメッセージを決めます。

try
{
    _orderService.ProcessOrder(orderId);
}
catch (Exception ex)
{
    // ここではユーザー向けのメッセージだけ表示
    ShowError("注文処理中にエラーが発生しました。時間をおいて再度お試しください。");
}
C#

この構成だと、

サービス層で「どの注文で落ちたか」を含めた詳細ログが残る
UI 層では「ユーザーにどう伝えるか」だけに集中できる

というきれいな役割分担になります。

ここでの重要ポイントは、
「ログを取る層」と「ユーザーに見せる層」を分け、その間を“再スロー”でつなぐ
という設計です。


再スローと「例外ラップ」の違い

「そのまま投げる」のか「包み直して投げる」のか

前のテーマで話した「例外ラップ」と、今回の「再スロー」はよくセットで出てきますが、役割が違います。

再スロー
同じ例外をそのまま上に投げる
スタックトレースを保つために throw; を使う

例外ラップ
元の例外に文脈を足して、新しい例外として投げる
throw new MyException("メッセージ", ex); の形

例えば、リポジトリ層では「ラップ」、サービス層では「再スロー」という組み合わせがよくあります。

// リポジトリ層:ラップする
public User GetUser(int id)
{
    try
    {
        return ActuallyGetUser(id);
    }
    catch (Exception ex)
    {
        throw new UserRepositoryException($"ユーザー取得に失敗しました。Id={id}", ex);
    }
}

// サービス層:ログを取って再スロー
public UserDto GetUserDto(int id)
{
    try
    {
        var user = _userRepository.GetUser(id);
        return Map(user);
    }
    catch (UserRepositoryException ex)
    {
        _logger.LogError(ex, "ユーザー取得処理でエラー。Id={Id}", id);
        throw;  // ここではラップせず、そのまま上に投げる
    }
}
C#

ここでの重要ポイントは、
「文脈を足したいときはラップ、ただ通したいときは再スロー」
という使い分けです。


「とりあえず catch して何もしない」は最悪パターン

例外を握りつぶすと、バグが“静かに”潜む

初心者がやりがちな危険パターンがこれです。

try
{
    DangerousOperation();
}
catch (Exception)
{
    // 何もしない
}
C#

一見「落ちなくなった」ように見えますが、実際には

処理が途中で止まっている
でも、ログもエラー表示も何もない
結果だけおかしい

という、最悪に調査しづらい状態になります。

本当に無視してよい例外はほとんどありません。
少なくとも、

ログを取る
ユーザーに何かしらのメッセージを出す
上の層に再スローする

のどれかはやるべきです。

ここでの重要ポイントは、
「catch したら、必ず“何か”する。何もしないなら catch 自体を書かないほうがマシ」
という強い意識を持つことです。
再スローは、その「何か」の代表的な選択肢です。


小さなユーティリティとしての「ログ+再スロー」パターン

毎回同じ書き方をしないための薄いラッパー

「ログを書いてから再スローする」パターンはよく出てくるので、
小さなユーティリティにしておくとコードがすっきりします。

public static class RethrowHelper
{
    public static void RunWithLogging(ILogger logger, Action action, string message)
    {
        try
        {
            action();
        }
        catch (Exception ex)
        {
            logger.LogError(ex, message);
            throw;  // ここがポイント
        }
    }
}
C#

使い方はこうです。

RethrowHelper.RunWithLogging(
    _logger,
    () => _orderRepository.Save(order),
    "注文保存中にエラーが発生しました。");
C#

ここでの重要ポイントは、
「ユーティリティの中でも、再スローは必ず throw; を使う」
ということです。
throw ex; にしてしまうと、スタックトレースがユーティリティの中から始まってしまいます。


まとめ:「例外再スロー」は“ログと責任のバトンリレー”をきれいにする技術

例外再スローの本質は、

いったん例外を受け止めて
必要な処理(ログなど)を行い
責任のある層に、元の情報を保ったままバトンを渡す

ことです。

特に押さえておきたいポイントは、次の 3 つです。

同じ例外を投げ直すときは、必ず throw; を使う。throw ex; はスタックトレースを壊す。
「ログを取ってから再スロー」は、サービス層などでよく使う実務パターン。
ラップしたいときは throw new XxxException("...", ex);、そのまま通したいときは throw; と使い分ける。

ここまで腹落ちしていれば、
「とりあえず catch して new Exception を投げ直す」段階から卒業して、
“ログと責任の流れを意識した例外処理”が書けるようになります。

タイトルとURLをコピーしました