はじめに:「例外ラップ」は“生のエラーを、そのまま外に漏らさないためのフィルター”
業務システムを書いていると、いろんな層で例外が飛びます。
DB アクセス、外部 API、ファイル I/O、ライブラリ内部…。
それを「そのまま上に投げる」と、上の層はこうなります。
「結局、何が起きたのか分からない」
「どの画面で、どんな業務処理中に落ちたのか分からない」
「ユーザーにそのまま技術的なメッセージが見えてしまう」
ここで効いてくるのが 例外ラップ(Exception Wrapping) です。
ざっくり言うと、
「下の層から来た生の例外を、そのまま投げずに“意味のある自分の例外”に包み直す」
というテクニックです。
ここでは、初心者向けに
なぜ例外をラップするのか
内側の例外を保持したままラップする書き方
業務例外とシステム例外を分けるイメージ
小さなユーティリティとしての「ラップ関数」の形
を、例題付きでかみ砕いて説明します。
なぜ例外をラップするのか
「どの層で、何をしていて、何に失敗したか」を伝えるため
例えば、リポジトリ層で DB にアクセスしているとします。
public User GetUser(int id)
{
using var conn = new SqlConnection(_connectionString);
conn.Open();
// ここで SqlException などが飛ぶ可能性がある
...
}
C#この SqlException をそのまま上に投げると、サービス層や UI 層は「SQL の詳細なエラー」を直接見ることになります。
でも、サービス層が本当に知りたいのは、
「ユーザー取得処理中に、データアクセスで失敗した」
という“文脈付きの情報”です。
例外ラップを使うと、こうなります。
public User GetUser(int id)
{
try
{
using var conn = new SqlConnection(_connectionString);
conn.Open();
...
}
catch (Exception ex)
{
throw new UserRepositoryException($"ユーザー取得に失敗しました。Id={id}", ex);
}
}
C#上の層から見ると、「UserRepositoryException が飛んできた」と分かります。
メッセージには「ユーザー取得に失敗」「Id=xxx」といった業務的な情報が入っています。
そして ex.InnerException を見れば、元の SqlException もちゃんと残っています。
ここでの重要ポイントは、「例外ラップは“文脈を足す”ためにやる」ということです。
単に例外の種類を変えるだけではなく、「どの処理で」「どんな入力で」失敗したかをメッセージに乗せます。
例外ラップの基本形:InnerException を必ず保持する
「元の例外を捨てない」が絶対ルール
例外をラップするときに一番やってはいけないのは、「元の例外を捨てる」ことです。
例えば、こういう書き方は NG に近いです。
catch (Exception)
{
throw new Exception("ユーザー取得に失敗しました");
}
C#これだと、元のスタックトレースやエラーメッセージが完全に失われます。
調査するときに、「どこで何が起きたか」が追えません。
正しいラップは、必ず InnerException に元の例外を渡します。
catch (Exception ex)
{
throw new UserRepositoryException("ユーザー取得に失敗しました", ex);
}
C#UserRepositoryException 側は、こう定義します。
public class UserRepositoryException : Exception
{
public UserRepositoryException(string message, Exception innerException)
: base(message, innerException)
{
}
}
C#これで、
外側の例外:業務的な文脈(どの処理で失敗したか)
内側の例外:技術的な詳細(SQL エラー、タイムアウトなど)
という二段構えになります。
ここでの重要ポイントは、「ラップするときは必ず InnerException に元の例外を渡す」ことです。
これを守るだけで、調査のしやすさが段違いになります。
業務例外とシステム例外を分ける
「ユーザーに見せるエラー」と「ログにだけ残すエラー」
例外ラップを使うとき、よくやる設計が「業務例外」と「システム例外」を分けることです。
業務例外
ユーザーの入力ミスや、業務ルール違反など。
「メッセージをそのまま画面に出してもよい」種類の例外。
システム例外
DB 障害、ネットワーク障害、バグなど。
ユーザーには「システムエラー」とだけ伝え、詳細はログに残す種類の例外。
例えば、サービス層でこう書きます。
public UserDto GetUser(int id)
{
try
{
var user = _userRepository.GetUser(id);
if (user == null)
{
throw new BusinessException($"ユーザーが見つかりません。Id={id}");
}
return Map(user);
}
catch (UserRepositoryException ex)
{
throw new SystemException("ユーザー情報の取得中にシステムエラーが発生しました。", ex);
}
}
C#UI 層では、例外の種類で分岐します。
try
{
var user = _userService.GetUser(id);
}
catch (BusinessException ex)
{
ShowMessage(ex.Message); // そのままユーザーに見せる
}
catch (SystemException ex)
{
_logger.LogError(ex, "ユーザー取得中にシステムエラー");
ShowMessage("システムエラーが発生しました。時間をおいて再度お試しください。");
}
C#ここでの重要ポイントは、「例外ラップを使って、“ユーザーに見せるメッセージ”と“ログに残す詳細”を分離する」ことです。
生の SqlException をそのまま画面に出すようなことは避けられます。
例外ラップをユーティリティ化する
「try-catch-throw のパターン」を共通化する
同じようなラップ処理を何度も書くのは面倒なので、小さなユーティリティにまとめることもできます。
例えば、「任意の処理を実行して、例外が出たら指定の例外型でラップする」メソッドです。
public static class ExceptionWrapper
{
public static void RunWithWrap<TException>(Action action, Func<Exception, TException> wrap)
where TException : Exception
{
try
{
action();
}
catch (Exception ex)
{
throw wrap(ex);
}
}
public static T RunWithWrap<TException, T>(Func<T> func, Func<Exception, TException> wrap)
where TException : Exception
{
try
{
return func();
}
catch (Exception ex)
{
throw wrap(ex);
}
}
}
C#使い方はこうです。
public User GetUser(int id)
{
return ExceptionWrapper.RunWithWrap(
() => ActuallyGetUser(id),
ex => new UserRepositoryException($"ユーザー取得に失敗しました。Id={id}", ex));
}
C#ここでの重要ポイントは、「ラップの“型”と“メッセージの作り方”だけを渡せばよい形にしておくと、例外ラップの書き忘れが減る」ことです。
ただし、初心者のうちはまず素直な try-catch-throw で慣れてから、共通化を考えるとよいです。
スタックトレースを壊さないための注意点
「throw ex;」ではなく「throw;」を使う場面との違い
例外ラップとよく混ざる話として、「throw ex; と throw; の違い」があります。
同じ例外をそのまま上に投げ直すときは、throw; を使うべきです。throw ex; と書くと、スタックトレースが「投げ直した場所」に書き換わってしまいます。
一方、例外ラップでは「新しい例外を投げる」ので、throw new XxxException(..., ex); になります。
この場合、外側の例外のスタックトレースはラップした場所から始まり、内側の例外(InnerException)に元のスタックトレースが残ります。
つまり、
そのまま再スローしたいとき → throw;
文脈を足してラップしたいとき → throw new MyException("...", ex);
という使い分けになります。
ここでの重要ポイントは、「ラップするときは“新しい例外を投げる”ので、スタックトレースの起点が変わる。その代わり InnerException に元の情報を残す」という理解を持つことです。
まとめ:「例外ラップ」は“エラーに文脈を与え、ユーザーとログをきれいに分ける技術”
例外ラップの本質は、
下の層から上がってきた生の例外に、
「どの処理で」「どんな入力で」失敗したかという文脈を足し、
ユーザーに見せる情報と、ログに残す情報をきれいに分けること
です。
押さえておきたいポイントを整理すると、次のようになります。
例外ラップは「文脈を足す」ために行う。どの処理で何に失敗したかをメッセージに含める。
ラップするときは必ず InnerException に元の例外を渡し、技術的な詳細を失わないようにする。
業務例外とシステム例外を分けることで、「ユーザーに見せるメッセージ」と「ログに残す詳細」を整理できる。
共通パターンは小さなユーティリティにまとめると、書き忘れやコピペが減る。
再スローとラップは別物であり、前者は throw;、後者は throw new XxxException(..., ex); を使う。
ここまで腹落ちしていれば、「とりあえず catch してそのまま throw new Exception」で終わらせる段階から抜けて、
“エラーに意味を与える設計”ができるエンジニアに一歩近づけます。
