C#において例外処理は、アプリケーションの堅牢性を確保するために欠かせない要素の一つです。
プログラムの実行中に予期しない状態に陥った際、適切に例外をスローすることで、バグの早期発見や安全なシステム停止が可能になります。
誤った例外の投げ方をすると、デバッグに必要な情報が失われたり、コードの可読性が著しく低下したりするリスクがあります。
本記事では、throw句の基本的な使い方から、2026年現在の開発現場で求められるベストプラクティスまでを詳しく解説します。
C#における例外スローの基本知識
例外をスローするとは、プログラムの実行中に問題が発生したことをランタイムや呼び出し元に通知する行為を指します。
C#では、throwキーワードを使用して例外オブジェクトを投げます。
throw句の基本的な構文
最もシンプルな例外のスローは、新しい例外インスタンスを生成してthrowに渡す形式です。
// 基本的な例外のスロー
public void SetAge(int age)
{
if (age < 0)
{
// ArgumentOutOfRangeExceptionをスローする
throw new ArgumentOutOfRangeException(nameof(age), "年齢は0以上である必要があります。");
}
}
このコードでは、引数が不正な場合に実行を中断し、呼び出し元にエラーの内容を伝えています。
例外をスローするタイミング
例外は、メソッドがその責任を果たせないことが確定したタイミングでスローするのが適切です。
例えば、必要なファイルが存在しない、ネットワーク接続が遮断されている、あるいは引数が無効であるといった状況が挙げられます。
「正常な制御フロー」の一部として例外を使用してはいけないという原則を忘れないでください。
標準的な例外クラスの使い分け
C#には、あらかじめ多くの例外クラスが用意されています。
独自の例外を作る前に、まずは.NETの標準ライブラリにあるクラスを利用できないか検討しましょう。
| 例外クラス名 | 主な用途 |
|---|---|
ArgumentNullException | メソッドの引数が null で、それが許容されない場合。 |
ArgumentException | 引数が null ではないが、無効な値である場合。 |
ArgumentOutOfRangeException | 引数の値が許容範囲外である場合。 |
InvalidOperationException | オブジェクトの現在の状態に対して、メソッドの呼び出しが無効な場合。 |
NotSupportedException | 呼び出された機能やメソッドがサポートされていない場合。 |
適切な例外クラスを選択することで、呼び出し側のコードはtry-catchブロックで特定の状況に応じた対処がしやすくなります。
再スローの正しい作法:throw vs throw ex
キャッチした例外を再度スローする場合、書き方によってプログラムの挙動が大きく変わります。
これはC#エンジニアが最も間違いやすいポイントの一つです。
スタックトレースを維持する throw;
catchブロック内で受け取った例外をそのまま上に投げたい場合は、throw; とだけ記述します。
try
{
// 何らかの処理
}
catch (Exception ex)
{
// ログ出力などの処理
logger.LogError(ex.Message);
// スタックトレースを保持したまま再スロー
throw;
}
この方法を用いると、最初に例外が発生した場所の情報(スタックトレース)が完全に保存されます。
スタックトレースを上書きしてしまう throw ex;
一方で、throw ex; のように例外変数を指定して再スローすると、その場所が起点としてスタックトレースが書き換えられます。
try
{
// 何らかの処理
}
catch (Exception ex)
{
// 推奨されない書き方:元の発生場所が不明になる
throw ex;
}
これでは、バグが発生した真の原因箇所を特定することが困難になるため、特別な理由がない限り避けるべきです。
モダンなC#における例外スローのテクニック
近年のC#(.NET 6以降から2026年現在の最新版まで)では、より簡潔かつ安全に例外を扱うための機能が追加されています。
ガード句による引数チェック
メソッドの冒頭で引数をチェックし、不正な場合に即座に例外を投げるパターンを「ガード句」と呼びます。
最新の.NETでは、ArgumentNullException.ThrowIfNull などの便利な静的メソッドが利用可能です。
public void ProcessData(string? input)
{
// C# 10以降のモダンな書き方
ArgumentNullException.ThrowIfNull(input);
// 処理の継続
Console.WriteLine(input.Length);
}
この書き方により、コードの可読性が向上し、タイポによるバグを防ぐことができます。
例外のフィルタリング(when句)
catchブロックにwhen句を追加することで、特定の条件を満たす場合にのみ例外を捕捉し、それ以外はスローし続けるといった制御が可能です。
try
{
ExecuteNetworkRequest();
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
// 404エラーの場合のみ特定の処理を行う
HandleNotFound();
}
不要な例外をキャッチしてから再スローする手間が省けるため、パフォーマンスとコードの明快さの両面でメリットがあります。
カスタム例外クラスの作成と活用
標準の例外クラスではドメイン固有のエラーを表現しきれない場合、独自の例外クラスを作成します。
カスタム例外の定義方法
カスタム例外を作成する際は、必ず Exception クラス(またはその派生クラス)を継承します。
// カスタム例外の定義例
public class OrderProcessingException : Exception
{
public int OrderId { get; }
public OrderProcessingException(string message, int orderId) : base(message)
{
OrderId = orderId;
}
public OrderProcessingException(string message, int orderId, Exception innerException)
: base(message, innerException)
{
OrderId = orderId;
}
}
2026年現在のモダンなC#開発では、プライマリコンストラクタを利用してより簡潔に記述することも一般的です。
Inner Exception(内部例外)の重要性
別の例外をラップして新しい例外を投げる場合は、必ず「元の例外」を引数に渡してください。
これを Inner Exception と呼び、エラーの連鎖を辿るための重要な手がかりになります。
try
{
// データベース接続
}
catch (SqlException ex)
{
// 下層の例外を包んで、上位のコンテキストに合わせた例外を投げる
throw new OrderProcessingException("注文処理中にデータエラーが発生しました。", orderId, ex);
}
例外スローにおけるパフォーマンスと運用の注意点
例外処理は非常に強力ですが、濫用するとシステム全体のパフォーマンスに悪影響を及ぼす可能性があります。
例外は「例外的な状況」のために使う
例外のスローとキャッチは、通常の条件分岐(if文など)に比べて計算コストが高い処理です。
ループ内での頻繁なスローは、処理速度の低下に直結します。
数値への変換が可能かどうかを確認する int.TryParse のような「Tester-Doerパターン」を活用し、例外を回避できる設計を心がけましょう。
意味のあるメッセージを含める
例外に含めるメッセージは、エンドユーザー向けではなく「開発者や運用担当者」に向けたものであるべきです。
「エラーが発生しました」といった抽象的な内容ではなく、「どのデータのどの値が原因で失敗したのか」がわかる情報を付与してください。
ただし、パスワードや個人情報などの機密情報を例外メッセージに含めないよう、セキュリティ面にも配慮が必要です。
まとめ
C#での例外のスローは、単にエラーを報告するだけでなく、プログラムの意図を明確にするための重要な設計要素です。
まずは標準の例外クラスを適切に使い分け、ガード句を活用してクリーンなコードを目指しましょう。
再スローの際は throw; を使用してスタックトレースを維持することが、デバッグの効率化において不可欠です。
また、カスタム例外や内部例外を活用することで、複雑なシステムでもエラーの原因を正確に特定できるようになります。
パフォーマンスへの影響を考慮しつつ、適切なタイミングで正しく例外をスローすることで、より高品質なアプリケーションを開発していきましょう。
