はじめに:「例外再スロー」は“いったん受け止めてから、ちゃんと投げ直す”テクニック
例外をキャッチしたあとに、こう思う場面がよくあります。
ログは取りたい
でも、この場では処理できない
だから、上の層に判断を任せたい
このときに使うのが 例外再スロー(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 を投げ直す」段階から卒業して、
“ログと責任の流れを意識した例外処理”が書けるようになります。
