アプリケーションの階層構造(レイヤー)において、下位の処理で発生した例外を上位の呼び出し元に伝播させたい場面は多々あります。その際、例外を単に右から左へ流すのではなく、適切に「再スロー」あるいは「ラップ(包み込み)」することで、デバッグのしやすさとコードの保守性が大きく向上します。
ここでは、ユーザープロファイルの更新処理を題材に、スタックトレースを維持したまま例外を再スローする方法と、低レベルな例外を業務的な例外に変換してスローする方法について解説します。
例外の伝播パターン
例外処理における再スローには、大きく分けて2つのアプローチがあります。
- そのまま再スロー(
throw;): ログ出力などの処理を行った後、発生した例外をそのまま上位へ投げます。 - ラップしてスロー(
throw new ...): 発生した例外をInnerExceptionに格納し、より抽象度の高い別の例外(業務例外など)として投げ直します。
実践的なコード例:プロファイル更新と画像処理
以下のコードは、ユーザーのアバター画像を読み込む処理(低レベル)と、それを呼び出すプロファイル更新処理(高レベル)の連携を示しています。
using System;
using System.IO;
namespace UserProfileSystem
{
// 独自の業務例外クラス
public class UserUpdateException : Exception
{
// InnerExceptionを受け取るコンストラクタを定義
public UserUpdateException(string message, Exception innerException)
: base(message, innerException)
{
}
}
class Program
{
static void Main()
{
try
{
// 上位レベルの処理呼び出し
UpdateUserAvatar("user_001", "missing_image.png");
}
catch (UserUpdateException ex)
{
// パターン1: ラップされた例外の捕捉
Console.WriteLine("=== 業務エラーが発生しました ===");
Console.WriteLine($"メッセージ: {ex.Message}");
// 元の原因(InnerException)を確認
if (ex.InnerException != null)
{
Console.WriteLine($"根本原因: {ex.InnerException.GetType().Name}");
Console.WriteLine($"詳細: {ex.InnerException.Message}");
}
}
catch (Exception ex)
{
// パターン2: 再スローされた例外の捕捉
Console.WriteLine("=== 予期せぬシステムエラー ===");
Console.WriteLine($"メッセージ: {ex.Message}");
// スタックトレースが維持されているか確認
Console.WriteLine($"スタックトレース:\n{ex.StackTrace}");
}
}
static void UpdateUserAvatar(string userId, string imagePath)
{
try
{
// 画像読み込み処理を実行
LoadImageFile(imagePath);
}
catch (FileNotFoundException ex)
{
// 【パターン1: 例外のラップ】
// 「ファイルがない」という事実を、「更新に失敗した」という業務的な意味に変換します。
// 第2引数に元の例外(ex)を渡すことで、InnerExceptionとして保持します。
throw new UserUpdateException($"ID: {userId} のアバター更新に失敗しました。", ex);
}
catch (UnauthorizedAccessException)
{
// 【パターン2: 例外の再スロー】
// ログ出力だけ行い、例外処理自体は上位に委ねます。
Console.WriteLine($"[Log] アクセス権限エラーが発生。Path: {imagePath}");
// 重要: "throw ex;" ではなく "throw;" を使用します。
throw;
}
}
// 下位レベルのファイル操作シミュレーション
static void LoadImageFile(string path)
{
// 説明用に強制的に例外を発生させます
if (path.Contains("missing"))
{
throw new FileNotFoundException("指定された画像ファイルが見つかりません。", path);
}
if (path.Contains("protected"))
{
throw new UnauthorizedAccessException("ファイルへのアクセスが拒否されました。");
}
}
}
}
実行結果
=== 業務エラーが発生しました ===
メッセージ: ID: user_001 のアバター更新に失敗しました。
根本原因: FileNotFoundException
詳細: 指定された画像ファイルが見つかりません。
技術的なポイントと注意点
1. throw; と throw ex; の決定的違い
例外をそのまま再スローする場合、必ず throw; を使用してください。
throw;(推奨):- 例外が発生した元の場所(
LoadImageFileメソッド)からのスタックトレース情報を維持します。デバッグ時に「真の発生源」を特定できます。
- 例外が発生した元の場所(
throw ex;(禁止):- スタックトレースがリセットされます。例外の発生場所が「再スローした行(
catchブロック内)」に書き換わってしまい、元の発生箇所が分からなくなります。
- スタックトレースがリセットされます。例外の発生場所が「再スローした行(
2. InnerExceptionによる情報の保全
例外を別の例外に変換(ラップ)する場合、必ずコンストラクタの第2引数などに元の例外を渡すように設計します。これにより、上位層では「業務的なエラーハンドリング」を行いつつ、ログ出力時にはInnerExceptionを辿って「技術的な詳細原因」を記録することが可能になります。
3. 抽象化レベルの統一
メソッドが投げる例外は、そのメソッドの抽象度に合わせるのが一般的です。例えば「ユーザー更新メソッド」が、内部実装の詳細である「SQLエラー」や「ファイル未検出エラー」をそのまま投げるよりは、UserUpdateExceptionのような専用例外にラップして投げた方が、呼び出し元(UI層など)のコードがシンプルになり、カプセル化が保たれます。
まとめ
例外処理の実装において、「再スロー」は情報の維持と隠蔽をコントロールする重要な手段です。
- 情報を維持したまま上位に渡すなら
throw; - 意味を変換して上位に渡すなら
throw new XxxException(msg, inner)
この2つを適切に使い分けることで、堅牢で保守性の高いエラーハンドリングを構築できます。
