C# でメソッドを書くとき、引数の不正や状態の不整合をどう知らせるかは設計の基本です。Debug.Assert のような「開発時だけのチェック」と異なり、例外はリリースビルドでも常に呼び出し側に意図を伝えます。
.NET には用途別の標準例外が豊富に用意されており、これらを正しく使い分けるだけでコードの意図が明確になり、呼び出し側も適切に対処できます。 この記事ではプログラマが日常的に使う代表的な標準例外を、用途別に整理します。
例外そのものの基礎は System.Exception を体系的に理解する を、when や AggregateException などの応用は 例外フィルター(when)と AggregateException・ExceptionDispatchInfo を参照してください。
引数チェック系
ArgumentException
引数の値が不正なときの汎用例外です。「null ではないが意味的に正しくない」場合に使います。
public void SetName(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("名前は空白にできません。", nameof(name));
}
// ...
}
引数名は必ず nameof() で渡します。後でリネームしてもコンパイラが追従してくれます。
ArgumentNullException
引数が null のときに使う、ArgumentException の派生です。最も使用頻度が高い例外の一つです。
public void Register(User user)
{
if (user is null)
{
throw new ArgumentNullException(nameof(user));
}
// ...
}
ThrowIfNull(.NET 6 以降)
.NET 6 で ArgumentNullException.ThrowIfNull が追加され、定型コードが大幅に短くなりました。
public void Register(User user)
{
ArgumentNullException.ThrowIfNull(user);
// ...
}
第二引数を省略すると、CallerArgumentExpression 属性により自動で引数名が補われます。
ArgumentOutOfRangeException
引数が許容範囲外のときに使います。配列インデックス、サイズ、列挙値の範囲チェックなどが典型例です。
public void SetAge(int age)
{
if (age < 0 || age > 150)
{
throw new ArgumentOutOfRangeException(
nameof(age), age, "年齢は 0〜150 の範囲で指定してください。");
}
// ...
}
ThrowIf* ヘルパー(.NET 7/8 以降)
.NET 8 で ArgumentOutOfRangeException にも豊富なヘルパーが追加されました。
public void Take(int count)
{
ArgumentOutOfRangeException.ThrowIfNegative(count);
ArgumentOutOfRangeException.ThrowIfGreaterThan(count, items.Count);
// ...
}
主なメソッド:
ThrowIfNegative/ThrowIfNegativeOrZeroThrowIfZeroThrowIfGreaterThan/ThrowIfGreaterThanOrEqualThrowIfLessThan/ThrowIfLessThanOrEqualThrowIfEqual/ThrowIfNotEqual
ArgumentException.ThrowIfNullOrEmpty / ThrowIfNullOrWhiteSpace
文字列引数のチェックも .NET 7/8 でヘルパーが整備されました。
public void SetName(string name)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
// ...
}
ThrowIfNullOrEmpty は .NET 7、ThrowIfNullOrWhiteSpace は .NET 8 で追加されました。
状態・操作系
InvalidOperationException
引数は正しいが、オブジェクトの現在の状態でその操作が行えないときに使います。
public void Disconnect()
{
if (!_isConnected)
{
throw new InvalidOperationException("接続されていません。");
}
// ...
}
LINQ の Single() / First() で要素がない場合や、IEnumerator.MoveNext() を呼ばずに Current を参照したときなど、BCL の中でも非常に多く使われています。
「引数の問題なら Argument*、状態の問題なら InvalidOperationException」と覚えるとわかりやすいです。
ObjectDisposedException
Dispose 済みのオブジェクトを操作しようとしたときに使います。InvalidOperationException の派生です。
public void Read()
{
ObjectDisposedException.ThrowIf(_disposed, this);
// ...
}
ObjectDisposedException.ThrowIf(.NET 7 以降)でこちらも簡潔に書けます。
NotSupportedException
その操作自体がサポートされていないときに使います。
読み取り専用コレクションの Add や、ストリームの Seek 非対応など、機能的に提供しないことを示すケースです。
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException("このストリームは読み取り専用です。");
}
InvalidOperationException との違いは、「現在の状態」ではなく「そもそも対応していない」点です。
NotImplementedException
「いずれ実装するが今は未実装」を示します。スケルトンコードや TODO に使います。
public void DoSomething()
{
throw new NotImplementedException();
}
リリースコードに残してはいけません。意図的に「対応しない」場合は NotSupportedException を使います。
I/O・フォーマット系
FormatException
文字列を別の型に変換しようとしたが、書式が不正だったときに使います。
if (!int.TryParse(text, out int value))
{
throw new FormatException($"数値として解釈できません: {text}");
}
int.Parse などが内部でスローします。自前のパーサーを書くときも、この例外を使うと利用者が Parse 系と統一的に扱えます。
OverflowException
算術演算や型変換の結果が型の範囲を超えたときに使います。
checked ブロック内の演算や、Convert.ToInt32(long) の範囲外などで自動的にスローされます。
checked
{
int sum = a + b; // オーバーフローすれば OverflowException
}
IOException 系
ファイルやネットワーク I/O のエラー全般。具体的には次の派生があります。
FileNotFoundException: ファイルが存在しないDirectoryNotFoundException: ディレクトリが存在しないPathTooLongException: パスが長すぎるEndOfStreamException: ストリームの末端を超えて読もうとした
呼び出し側で個別対処したい場合は派生型を catch し、まとめて扱う場合は IOException で捕まえます。
並行・キャンセル系
OperationCanceledException
CancellationToken によってキャンセルされたときの例外です。
TaskCanceledException はこの派生で、Task のキャンセル時にスローされます。
public async Task DoWorkAsync(CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
// ...
}
CancellationToken.ThrowIfCancellationRequested() を呼ぶだけでよく、自分で new する場面は少ないです。
キャンセルは「想定内のフロー」なので、ログレベルもエラーではなく情報や警告で扱うことが多いです。
TimeoutException
タイムアウト時の例外。HTTP クライアントや非同期 API で使われます。 自前の処理でタイムアウトを表現する場合にも使えます。
型・キャスト系
InvalidCastException
明示的キャストが失敗したときにスローされます。
通常は as 演算子と null チェック、is 演算子、パターンマッチングで回避できます。
// キャストできなければ obj が null になる
var str = obj as string;
// パターンマッチング
if (obj is string s)
{
// s を使う
}
NullReferenceException
null 参照のメンバーアクセスで CLR が自動的にスローします。
自分で throw new NullReferenceException() してはいけません。意図的なら ArgumentNullException か InvalidOperationException を使います。
NullReferenceException がアプリで起きる時点で、ほとんどの場合はバグです。Nullable 参照型(string?)を有効にして、コンパイル時に検出するのが現代的な対策です。
IndexOutOfRangeException
配列やリストへのインデックスアクセスが範囲外だったときに CLR がスローします。
NullReferenceException と同様、自前でスローするのではなく、自前のメソッドで範囲チェックする場合は ArgumentOutOfRangeException を使います。
標準例外の選び方フローチャート
迷ったときの判断基準をまとめます。
| 状況 | 使う例外 |
|---|---|
引数が null |
ArgumentNullException |
| 引数が範囲外 | ArgumentOutOfRangeException |
| その他の引数不正 | ArgumentException |
| オブジェクトの状態が不正 | InvalidOperationException |
Dispose 済み |
ObjectDisposedException |
| そもそも非対応 | NotSupportedException |
| 未実装(仮置き) | NotImplementedException |
| 文字列の書式不正 | FormatException |
| 算術オーバーフロー | OverflowException |
| キャンセル | OperationCanceledException |
| タイムアウト | TimeoutException |
| ファイル不在 | FileNotFoundException |
| I/O 全般 | IOException |
使うべきでない・自前でスローしないもの
CLR がスローする以下の例外は、アプリコードから直接スローしないのが原則です。
NullReferenceException:null参照アクセスで CLR がスローするIndexOutOfRangeException: 配列の範囲外アクセスで CLR がスローするAccessViolationException: アンマネージド領域の不正アクセスOutOfMemoryException: メモリ不足StackOverflowException: スタックオーバーフロー(catch も基本的にできない)Exception/SystemException/ApplicationException: 抽象度が高すぎる
これらを自前でスローすると、呼び出し側が「CLR からの本物の障害」と区別できなくなります。
まとめ
- 引数チェックは
ArgumentNullException/ArgumentOutOfRangeException/ArgumentExceptionを用途で使い分ける - 状態の不整合は
InvalidOperationException、Dispose済みはObjectDisposedException - 機能未対応は
NotSupportedException、TODO はNotImplementedException - .NET 6/7/8 で追加された
ThrowIfNull/ThrowIfNegative/ThrowIfNullOrWhiteSpaceなどのヘルパーで定型コードを大幅に削減できる NullReferenceExceptionやIndexOutOfRangeExceptionは自前でスローしない- 引数名は必ず
nameof()で渡す
「正しい例外を選ぶ」ことは、ライブラリ設計だけでなくアプリ開発でもコードの意図を強く表す手段です。標準例外の語彙を増やしておくと、レビューや障害調査の質が上がります。