throw / カスタム例外
| 対応: | C# 1.0(2002) |
|---|
例外を意図的に発生させる『throw』キーワードと、独自のエラー情報を持つカスタム例外クラスの定義方法です。
構文
// 例外をスローします。
throw new ExceptionType("メッセージ");
// カスタム例外クラスを定義します。
class ClassName : Exception
{
public ClassName(string message) : base(message) { }
}
構文一覧
| 構文 / メンバー | 概要 |
|---|---|
| throw new Exception("msg") | 例外をスローします。現在のメソッド呼び出しを中断し、最も近い catch ブロックに制御を移します。 |
| throw; | catch ブロック内で使用し、捕捉した例外を再スローします(スタックトレースを保持します)。 |
| Exception | すべての例外の基底クラスです。 |
| ArgumentException | 引数が無効な場合にスローします。 |
| ArgumentNullException | 引数が null の場合にスローします。 |
| InvalidOperationException | オブジェクトの状態に対して無効な操作を行った場合にスローします。 |
| NotImplementedException | 未実装のメソッドを示す場合に使います。 |
サンプルコード
Program.cs
using System;
// throw で例外をスローします。
static void CheckAge(int age)
{
if (age < 0 || age > 150)
throw new ArgumentOutOfRangeException(nameof(age), "年齢は 0〜150 の範囲で指定してください。");
Console.WriteLine($"年齢: {age}");
}
try
{
CheckAge(200);
}
catch (ArgumentOutOfRangeException e)
{
Console.WriteLine(e.Message);
}
// カスタム例外クラスを定義します。
class AppException : Exception
{
public int StatusCode { get; }
public AppException(string message, int statusCode) : base(message)
{
StatusCode = statusCode;
}
}
// カスタム例外をスローして捕捉します。
try
{
throw new AppException("認証に失敗しました。", 401);
}
catch (AppException e)
{
Console.WriteLine($"[{e.StatusCode}] {e.Message}");
}
// catch 内で再スローします(スタックトレースが保持されます)。
static void Process()
{
try
{
int result = int.Parse("abc");
}
catch (FormatException)
{
Console.WriteLine("ログ: 変換エラーを検出しました。");
throw; // 例外を再スローします(スタックトレースを保持)。
}
}
try
{
Process();
}
catch (FormatException e)
{
Console.WriteLine("上位で捕捉: " + e.Message);
}
dotnet script throw_custom_exception.csx 年齢は 0〜150 の範囲で指定してください。 (Parameter 'age') [401] 認証に失敗しました。 ログ: 変換エラーを検出しました。 上位で捕捉: The input string 'abc' was not in a correct format.
実践パターン: 詳細情報を持つカスタム例外
アプリケーション固有のエラーコードや追加情報をプロパティとして持たせたカスタム例外クラスの実装例です。
CustomExceptions.cs
using System;
using System.Collections.Generic;
// API エラーを表すカスタム例外です。
class ApiException : Exception
{
public int StatusCode { get; }
public string ErrorCode { get; }
public object? Detail { get; }
public ApiException(string message, int statusCode, string errorCode, object? detail = null)
: base(message)
{
StatusCode = statusCode;
ErrorCode = errorCode;
Detail = detail;
}
}
// バリデーションエラーを表すカスタム例外です。
class ValidationException : Exception
{
public IReadOnlyList<string> Errors { get; }
public ValidationException(IReadOnlyList<string> errors)
: base($"バリデーションエラー: {errors.Count}件")
{
Errors = errors;
}
}
// API 呼び出しをシミュレートします。
static string FetchUser(int id)
{
if (id <= 0)
throw new ApiException("無効な ID です。", 400, "INVALID_ID", new { Id = id });
if (id > 100)
throw new ApiException("ユーザーが見つかりません。", 404, "USER_NOT_FOUND");
return id switch
{
1 => "user_01",
2 => "user_02",
_ => $"User-{id}"
};
}
// ApiException の捕捉
try { Console.WriteLine(FetchUser(-1)); }
catch (ApiException e)
{
Console.WriteLine($"API エラー [{e.StatusCode}] {e.ErrorCode}: {e.Message}");
if (e.Detail != null) Console.WriteLine($" 詳細: {e.Detail}");
}
// ValidationException の捕捉
var errors = new List<string> { "名前は必須です。", "年齢は 0〜150 の範囲で入力してください。" };
try { throw new ValidationException(errors); }
catch (ValidationException e)
{
Console.WriteLine(e.Message);
foreach (string err in e.Errors)
Console.WriteLine($" - {err}");
}
dotnet script custom_exceptions.csx
API エラー [400] INVALID_ID: 無効な ID です。
詳細: { Id = -1 }
バリデーションエラー: 2件
- 名前は必須です。
- 年齢は 0〜150 の範囲で入力してください。
実践パターン: throw 式(C# 7 以降)
C# 7 以降では throw を式として使えます。三項演算子や null 合体演算子と組み合わせると簡潔に書けます。
ThrowExpression.cs
using System;
// throw 式: 三項演算子と組み合わせます。
static int Divide(int a, int b)
=> b == 0
? throw new DivideByZeroException("0 で割ることはできません。")
: a / b;
// throw 式: null 合体演算子と組み合わせます。
static string GetName(string? name)
=> name ?? throw new ArgumentNullException(nameof(name), "名前は必須です。");
// throw 式: プロパティの setter でバリデーション
class MissionRecord
{
private string _title = string.Empty;
public string Title
{
get => _title;
set => _title = string.IsNullOrWhiteSpace(value)
? throw new ArgumentException("タイトルは空にできません。", nameof(value))
: value;
}
}
// テスト
Console.WriteLine(Divide(10, 2)); // 5
try { Divide(10, 0); } catch (DivideByZeroException e) { Console.WriteLine(e.Message); }
Console.WriteLine(GetName("sample")); // sample
try { GetName(null); } catch (ArgumentNullException e) { Console.WriteLine(e.Message); }
var mission = new MissionRecord();
try
{
mission.Title = ""; // setter で例外が発生します。
}
catch (ArgumentException e)
{
Console.WriteLine(e.Message);
}
dotnet script throw_expression.csx 5 0 で割ることはできません。 sample 名前は必須です。 (Parameter 'name') タイトルは空にできません。 (Parameter 'value')
よくあるミス
よくあるミス1: throw e でスタックトレースが上書きされる
catch ブロックで例外を再スローする場合、『throw e;』と書くとスタックトレースが現在の位置に上書きされます。スタックトレースを保持したい場合は引数なしの『throw;』を使います。
using System;
static void Inner() => throw new Exception("内部エラー");
static void Outer()
{
try { Inner(); }
catch (Exception e)
{
// NG: throw e; はスタックトレースを Outer の行に上書きします。
throw e; // Inner の情報が失われます。
}
}
static void OuterSafe()
{
try { Inner(); }
catch
{
// OK: throw; はスタックトレースを保持します。
throw;
}
}
よくあるミス2: Exception を直接スローする
汎用の Exception をそのままスローすると、呼び出し元が例外の種類を判別できません。適切な例外クラスを選ぶか、カスタム例外を定義します。
using System;
// NG: 汎用 Exception では呼び出し元が内容を把握できません。
static void ProcessNG(string input)
{
if (input == null)
throw new Exception("input が null です。"); // 型から何も読み取れません。
}
// OK: 状況に合った例外クラスを使います。
static void ProcessOK(string? input)
{
if (input == null)
throw new ArgumentNullException(nameof(input)); // 引数 null エラーと明確にわかります。
if (input.Length == 0)
throw new ArgumentException("input は空にできません。", nameof(input));
}
try { ProcessOK(null); }
catch (ArgumentNullException e) { Console.WriteLine(e.Message); }
dotnet run Value cannot be null. (Parameter 'input')
概要
カスタム例外クラスは『Exception』(または既存の例外クラス)を継承して定義します。アプリケーション固有のエラーコードや追加情報をプロパティとして持たせることで、catch 側での詳細なエラーハンドリングが可能になります。
再スローは『throw e;』と書くとスタックトレースが新しい位置に上書きされてしまうため、スタックトレースを保持したい場合は引数なしの『throw;』を使います。
例外の捕捉構文は『try / catch / finally』を参照してください。
記事の間違いや著作権の侵害等ございましたらお手数ですがこちらまでご連絡頂ければ幸いです。