C#の例外処理を基礎から解説|try-catchの使い方・エラー原因・実践的な対処法

はじめに

C#でアプリケーションを開発していると、実行中に「オブジェクト参照がオブジェクト インスタンスに設定されていません」「インデックスが配列の境界外です」「入力文字列の形式が正しくありません」といったエラーに遭遇することがあります。これらの多くは、C#における「例外」によって通知されています。

例外処理は、C#を学ぶうえで避けて通れない重要なテーマです。例外を正しく理解していないと、プログラムが突然停止したり、原因の特定に時間がかかったり、ユーザーに不親切なエラー画面を表示してしまうことがあります。

この記事では、C#の例外処理について、基礎知識からtry-catchの使い方、よく発生する例外の種類、原因調査の方法、実践的な対処法、ベストプラクティスまでわかりやすく解説します。

1. C#の例外とは?基礎知識をわかりやすく解説

1-1. 例外の意味とエラーとの違い

C#における例外とは、プログラムの実行中に発生した「通常の処理を続けられない問題」を表す仕組みです。

たとえば、存在しないファイルを読み込もうとしたり、nullのオブジェクトにアクセスしたり、数値に変換できない文字列を変換しようとしたりすると、例外が発生します。

一般的に「エラー」という言葉は広い意味で使われます。文法ミスのようにコンパイル時に検出されるものもエラーですし、実行中に発生する問題もエラーと呼ばれます。一方、例外は主に実行時に発生し、C#の例外処理機構によって扱える問題を指します。

たとえば、以下のような違いがあります。

C#
// コンパイルエラーの例
int number = "abc";

これは型が一致しないため、プログラムを実行する前にエラーになります。

C#
// 実行時例外の例
string text = null;
Console.WriteLine(text.Length);

こちらは文法上は正しいためコンパイルできますが、実行するとNullReferenceExceptionが発生します。

1-2. C#で例外が発生する仕組み

C#では、問題が発生すると例外オブジェクトが生成されます。例外オブジェクトには、例外の種類、エラーメッセージ、発生場所、原因となった内部例外などの情報が含まれます。

例外が発生すると、通常の処理の流れは中断されます。その後、発生した例外を処理できるcatchブロックが探されます。該当するcatchが見つかれば、その中の処理が実行されます。見つからない場合、例外は呼び出し元へ伝播していき、最終的にアプリケーションが終了することがあります。

C#
try
{
int[] numbers = { 1, 2, 3 };
Console.WriteLine(numbers[5]);
}
catch (IndexOutOfRangeException ex)
{
Console.WriteLine("配列の範囲外にアクセスしました。");
Console.WriteLine(ex.Message);
}

この例では、存在しないインデックスにアクセスしたためIndexOutOfRangeExceptionが発生します。しかしcatchで処理しているため、プログラム全体が突然停止することを防げます。

1-3. 例外処理が必要になる理由

例外処理が必要な理由は、プログラムを安全に動かし続けるためです。

アプリケーションでは、開発者が完全には制御できない状況がよくあります。たとえば、ファイルが削除されている、ネットワークが切断されている、ユーザーが不正な値を入力する、外部APIがエラーを返す、といったケースです。

これらを考慮せずにコードを書くと、少し想定外のことが起きただけでプログラムが停止してしまいます。例外処理を行うことで、エラーを検知し、ログを残し、ユーザーにわかりやすいメッセージを表示し、必要であれば処理をリトライできます。

1-4. 例外を放置した場合に起こること

C#で例外を適切に処理しない場合、アプリケーションが異常終了する可能性があります。コンソールアプリであればエラーメッセージが表示されて終了し、Webアプリであれば500エラーが発生し、デスクトップアプリであればユーザーにクラッシュ画面が表示されることもあります。

また、例外を放置すると次のような問題につながります。

・ユーザーが何をすればよいかわからない
・障害発生時に原因を調査できない
・ファイルやネットワーク接続などのリソースが解放されない
・データの保存途中で処理が止まり、不整合が起きる
・セキュリティ上見せるべきでない内部情報が表示される

例外処理は、単にエラーを隠すためのものではありません。問題が起きたときに安全に対処するための仕組みです。

2. C#でよく発生する例外の種類

2-1. NullReferenceException

NullReferenceExceptionは、C#で特によく発生する例外です。nullの変数に対してプロパティやメソッドを呼び出そうとしたときに発生します。

C#
string name = null;
Console.WriteLine(name.Length);

このコードでは、nameがnullであるにもかかわらずLengthプロパティにアクセスしているため、NullReferenceExceptionが発生します。

対処法としては、nullチェックを行う、null条件演算子を使う、nullable参照型を有効にする、といった方法があります。

C#
string? name = null;

if (name != null)
{
Console.WriteLine(name.Length);
}
else
{
Console.WriteLine("nameはnullです。");
}

また、null条件演算子を使うと簡潔に書けます。

C#
Console.WriteLine(name?.Length);

2-2. IndexOutOfRangeException

IndexOutOfRangeExceptionは、配列の範囲外にアクセスしたときに発生します。

C#
int[] numbers = { 10, 20, 30 };
Console.WriteLine(numbers[3]);

配列のインデックスは0から始まるため、この配列でアクセスできるのはnumbers[0]からnumbers[2]までです。numbers[3]は存在しないため例外になります。

対処法としては、Lengthを使って範囲を確認します。

C#
int index = 3;

if (index >= 0 && index < numbers.Length)
{
Console.WriteLine(numbers[index]);
}
else
{
Console.WriteLine("指定されたインデックスは範囲外です。");
}

2-3. ArgumentException/ArgumentNullException

ArgumentExceptionは、メソッドに渡された引数が不正な場合に発生します。ArgumentNullExceptionは、nullを許可していない引数にnullが渡された場合に使われます。

C#
void PrintName(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("名前が空です。", nameof(name));
}

Console.WriteLine(name);
}

ArgumentNullExceptionを使う例は以下です。

C#
void Save(string filePath)
{
if (filePath == null)
{
throw new ArgumentNullException(nameof(filePath));
}

Console.WriteLine($"保存先: {filePath}");
}

C#では、メソッドの前提条件を満たしていない場合にArgumentException系の例外を使うことがよくあります。

2-4. InvalidOperationException

InvalidOperationExceptionは、オブジェクトの状態が現在の操作に適していない場合に発生します。

たとえば、要素が存在しないコレクションに対してFirstを呼び出すと発生します。

C#
List<int> numbers = new List<int>();
int first = numbers.First();

この場合、リストが空なので最初の要素を取得できません。対処法としてFirstOrDefaultを使ったり、Anyで事前確認したりします。

C#
if (numbers.Any())
{
int first = numbers.First();
Console.WriteLine(first);
}
else
{
Console.WriteLine("リストに要素がありません。");
}

2-5. FormatException

FormatExceptionは、文字列の形式が期待した形式と異なる場合に発生します。数値や日付への変換時によく見られます。

C#
string input = "abc";
int number = int.Parse(input);

"abc"は整数に変換できないため、FormatExceptionが発生します。

ユーザー入力の変換では、int.Parseよりもint.TryParseを使うほうが安全です。

C#
string input = "abc";

if (int.TryParse(input, out int number))
{
Console.WriteLine(number);
}
else
{
Console.WriteLine("数値を入力してください。");
}

2-6. IOException

IOExceptionは、ファイルやストリームなどの入出力処理で問題が起きた場合に発生します。

たとえば、ファイルが存在しない、アクセス権がない、別のプロセスがファイルを使用している、ディスクに空き容量がない、といったケースです。

C#
try
{
string text = File.ReadAllText("sample.txt");
Console.WriteLine(text);
}
catch (IOException ex)
{
Console.WriteLine("ファイルの読み込み中にエラーが発生しました。");
Console.WriteLine(ex.Message);
}

ファイル操作は外部環境の影響を受けやすいため、例外処理が特に重要です。

2-7. DivideByZeroException

DivideByZeroExceptionは、整数を0で割ろうとしたときに発生します。

C#
int x = 10;
int y = 0;
int result = x / y;

0除算は計算できないため例外になります。対処法は、割る前に分母が0でないか確認することです。

C#
if (y != 0)
{
int result = x / y;
Console.WriteLine(result);
}
else
{
Console.WriteLine("0で割ることはできません。");
}

3. C#の例外処理の基本|try-catchの使い方

3-1. try-catchの基本構文

C#の例外処理では、try-catchを使います。例外が発生する可能性のある処理をtryブロックに書き、例外が発生した場合の処理をcatchブロックに書きます。

C#
try
{
// 例外が発生する可能性のある処理
}
catch
{
// 例外が発生したときの処理
}

実際の例は以下です。

C#
try
{
int number = int.Parse("abc");
Console.WriteLine(number);
}
catch
{
Console.WriteLine("数値への変換に失敗しました。");
}

このコードでは、int.ParseでFormatExceptionが発生しますが、catchによって処理されます。

3-2. 例外をcatchで受け取る方法

catchでは、発生した例外オブジェクトを受け取ることができます。例外オブジェクトを受け取ると、エラーメッセージやスタックトレースを確認できます。

C#
try
{
int number = int.Parse("abc");
}
catch (FormatException ex)
{
Console.WriteLine("形式が正しくありません。");
Console.WriteLine(ex.Message);
}

catchのかっこ内に例外型と変数名を書きます。この例では、FormatException型の例外をexという変数で受け取っています。

3-3. Exceptionクラスで例外情報を確認する

C#の例外は、基本的にExceptionクラスを継承しています。そのため、Exception型でcatchすると多くの例外をまとめて受け取れます。

C#
try
{
string text = null;
Console.WriteLine(text.Length);
}
catch (Exception ex)
{
Console.WriteLine(ex.GetType().Name);
Console.WriteLine(ex.Message);
Console.WriteLine(ex.StackTrace);
}

Exceptionクラスには、主に以下のような情報があります。

Messageはエラーメッセージです。
StackTraceは例外が発生した呼び出し履歴です。
InnerExceptionは内部で発生した元の例外です。
GetType()を使うと、実際の例外型を確認できます。

ただし、何でもExceptionでcatchすると原因ごとの適切な処理が書きにくくなります。基本的には、具体的な例外型をcatchすることが推奨されます。

3-4. 複数のcatchを使い分ける方法

C#では、複数のcatchを使って例外の種類ごとに処理を分けられます。

C#
try
{
string input = Console.ReadLine();
int number = int.Parse(input);
int result = 100 / number;

Console.WriteLine(result);
}
catch (FormatException)
{
Console.WriteLine("数値の形式が正しくありません。");
}
catch (DivideByZeroException)
{
Console.WriteLine("0で割ることはできません。");
}
catch (Exception ex)
{
Console.WriteLine("予期しないエラーが発生しました。");
Console.WriteLine(ex.Message);
}

このように書くことで、入力形式のエラーと0除算のエラーを別々に扱えます。

3-5. catchの順番で注意すべきポイント

複数のcatchを書く場合、順番に注意が必要です。C#では、上から順番にcatchが評価されます。そのため、より具体的な例外型を先に書き、より一般的な例外型を後に書く必要があります。

C#
try
{
int number = int.Parse("abc");
}
catch (FormatException)
{
Console.WriteLine("形式エラーです。");
}
catch (Exception)
{
Console.WriteLine("その他のエラーです。");
}

これは正しい順番です。

一方、以下のようにExceptionを先に書くと、FormatExceptionのcatchに到達できません。

C#
try
{
int number = int.Parse("abc");
}
catch (Exception)
{
Console.WriteLine("すべての例外を受け取ります。");
}
// catch (FormatException)
// {
// Console.WriteLine("ここには到達できません。");
// }

親クラスの例外を先にcatchしてしまうと、子クラスの例外を個別に処理できなくなります。catchは具体的なものから一般的なものへ並べるのが基本です。

4. finally・throw・usingを使った例外処理

4-1. finallyの役割と使いどころ

finallyは、例外が発生してもしなくても最後に実行されるブロックです。主にリソースの解放や後片付けに使います。

C#
try
{
Console.WriteLine("処理を開始します。");
int number = int.Parse("abc");
}
catch (FormatException)
{
Console.WriteLine("変換に失敗しました。");
}
finally
{
Console.WriteLine("後片付けを行います。");
}

このコードでは、int.Parseで例外が発生しますが、catchの後にfinallyが実行されます。

finallyは、ファイルを閉じる、データベース接続を解放する、一時的に変更した状態を元に戻す、といった用途で使われます。

4-2. throwで例外を発生させる方法

C#では、throwを使って意図的に例外を発生させることができます。メソッドの引数が不正な場合や、業務ルールに違反している場合などに使います。

C#
void RegisterUser(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("ユーザー名は必須です。", nameof(name));
}

Console.WriteLine($"登録しました: {name}");
}

throwを使うことで、「この状態では処理を続けられない」ということを呼び出し元に明確に伝えられます。

4-3. throwとthrow exの違い

catchした例外を再スローする場合、throw;throw ex;には重要な違いがあります。

C#
try
{
int number = int.Parse("abc");
}
catch (Exception)
{
throw;
}

throw;は、元のスタックトレースを保持したまま例外を再スローします。

一方、以下のようにthrow ex;と書くと、再スローした場所からスタックトレースが始まるため、元の発生箇所がわかりにくくなることがあります。

C#
try
{
int number = int.Parse("abc");
}
catch (Exception ex)
{
throw ex;
}

基本的に、catchした例外をそのまま再スローする場合はthrow;を使います。例外の原因を調査しやすくするためにも、throw ex;は避けるのが無難です。

4-4. using文によるリソース解放

ファイル、ストリーム、データベース接続などは、使い終わったら解放する必要があります。C#ではusing文を使うことで、例外が発生しても自動的にリソースを解放できます。

C#
using (StreamReader reader = new StreamReader("sample.txt"))
{
string text = reader.ReadToEnd();
Console.WriteLine(text);
}

usingブロックを抜けると、自動的にDisposeメソッドが呼び出されます。ブロック内で例外が発生した場合でも、Disposeが呼ばれるため安全です。

C# 8.0以降では、using宣言も使えます。

C#
using StreamReader reader = new StreamReader("sample.txt");

string text = reader.ReadToEnd();
Console.WriteLine(text);

この場合、変数のスコープを抜けるタイミングでDisposeされます。

4-5. IDisposableと例外処理の関係

using文で自動解放できるオブジェクトは、IDisposableインターフェイスを実装しています。IDisposableにはDisposeメソッドが定義されており、リソースの解放処理を行います。

C#
public class SampleResource : IDisposable
{
public void Dispose()
{
Console.WriteLine("リソースを解放しました。");
}
}

usingを使うと、finallyでDisposeを呼ぶ処理を簡潔に書けます。

C#
using (var resource = new SampleResource())
{
Console.WriteLine("リソースを使用しています。");
}

これは概念的には以下のような処理に近いです。

C#
SampleResource resource = new SampleResource();

try
{
Console.WriteLine("リソースを使用しています。");
}
finally
{
resource.Dispose();
}

例外処理では、エラーへの対応だけでなく、リソースを確実に解放することも重要です。

5. 例外の原因を特定する方法

5-1. エラーメッセージの読み方

例外が発生したら、まずエラーメッセージを確認します。C#の例外メッセージには、何が問題だったのかを示す情報が含まれています。

C#
try
{
int number = int.Parse("abc");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}

たとえば、FormatExceptionであれば「入力文字列の形式が正しくありません」のようなメッセージが表示されます。

ただし、エラーメッセージだけで原因を完全に特定できるとは限りません。どの行で、どの値によって、どのような流れで例外が発生したのかを合わせて確認する必要があります。

5-2. スタックトレースの確認方法

スタックトレースは、例外が発生した場所までの呼び出し履歴です。どのメソッドの何行目で例外が発生したのかを調べるうえで非常に重要です。

C#
try
{
Execute();
}
catch (Exception ex)
{
Console.WriteLine(ex.StackTrace);
}

void Execute()
{
int number = int.Parse("abc");
}

スタックトレースを見ると、例外がどのメソッドから発生し、どの呼び出し経路を通ってきたのかがわかります。

例外調査では、メッセージよりもスタックトレースのほうが重要になることもあります。特に大きなアプリケーションでは、同じ例外型が複数の場所で発生するため、発生箇所の特定が欠かせません。

5-3. Visual Studioで例外をデバッグする方法

Visual Studioを使うと、例外が発生した瞬間にデバッガーで停止できます。これにより、変数の値や呼び出し履歴を確認しながら原因を調査できます。

例外発生時に確認したいポイントは次のとおりです。

・どの行で例外が発生したか
・変数に想定外の値が入っていないか
・nullになっているオブジェクトはないか
・配列やリストの件数は想定どおりか
・外部ファイルやAPIの戻り値は正しいか

Visual Studioでは、例外設定を使って特定の例外がスローされた瞬間に停止することもできます。try-catchで捕捉される例外でも、発生箇所で止められるため、原因調査がしやすくなります。

5-4. ブレークポイントを使った原因調査

ブレークポイントは、プログラムを指定した行で一時停止させる機能です。例外が起きる前の状態を確認するのに役立ちます。

たとえば、NullReferenceExceptionが発生している場合、例外が発生する行の直前にブレークポイントを置きます。そして、対象の変数がnullになっていないかを確認します。

C#
User user = GetUser();

Console.WriteLine(user.Name);

このコードでNullReferenceExceptionが発生する場合、userがnullの可能性があります。ブレークポイントで停止し、ウォッチウィンドウやローカルウィンドウで値を確認します。

原因調査では、例外が起きた行だけでなく、その値がどこで設定されたのかを追うことが大切です。

5-5. InnerExceptionの確認方法

InnerExceptionは、ある例外の内側にある元の例外を表します。特に、例外を別の例外で包んで再スローする場合に使われます。

C#
try
{
int number = int.Parse("abc");
}
catch (FormatException ex)
{
throw new InvalidOperationException("数値変換処理に失敗しました。", ex);
}

この場合、外側の例外はInvalidOperationExceptionですが、元の原因はFormatExceptionです。

InnerExceptionを確認するには、次のようにします。

C#
catch (Exception ex)
{
Console.WriteLine(ex.Message);

if (ex.InnerException != null)
{
Console.WriteLine("InnerException:");
Console.WriteLine(ex.InnerException.Message);
}
}

外側の例外だけを見ると原因がぼやけることがあります。InnerExceptionまで確認することで、本当の発生原因を見つけやすくなります。

6. 実践で使えるC#の例外処理パターン

6-1. ファイル読み込み時の例外処理

ファイル読み込みでは、ファイルが存在しない、アクセス権がない、読み込み中に問題が発生するなど、さまざまな例外が考えられます。

C#
try
{
string path = "data.txt";
string content = File.ReadAllText(path);

Console.WriteLine(content);
}
catch (FileNotFoundException)
{
Console.WriteLine("指定されたファイルが見つかりません。");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("ファイルにアクセスする権限がありません。");
}
catch (IOException ex)
{
Console.WriteLine("ファイル操作中にエラーが発生しました。");
Console.WriteLine(ex.Message);
}

ファイル操作では、具体的な例外型を分けて処理すると、ユーザーに適切なメッセージを表示しやすくなります。

6-2. 数値変換時の例外処理

ユーザー入力を数値に変換する場合、例外を使うよりTryParseを使うほうが適しています。

C#
Console.Write("年齢を入力してください: ");
string input = Console.ReadLine();

if (int.TryParse(input, out int age))
{
Console.WriteLine($"年齢は{age}歳です。");
}
else
{
Console.WriteLine("正しい数値を入力してください。");
}

以下のようにint.Parseをtry-catchで囲むこともできます。

C#
try
{
int age = int.Parse(input);
Console.WriteLine($"年齢は{age}歳です。");
}
catch (FormatException)
{
Console.WriteLine("数値の形式が正しくありません。");
}

しかし、入力ミスのように頻繁に起こることが予想される場合は、TryParseによる事前チェックのほうが読みやすく、効率的です。

6-3. 配列・リスト操作時の例外処理

配列やリストでは、範囲外アクセスに注意が必要です。

C#
List<string> names = new List<string> { "Alice", "Bob", "Charlie" };
int index = 5;

if (index >= 0 && index < names.Count)
{
Console.WriteLine(names[index]);
}
else
{
Console.WriteLine("指定された番号のデータは存在しません。");
}

範囲外アクセスをtry-catchで処理することもできますが、基本的には事前に件数を確認するほうが適切です。

また、LINQを使う場合はFirstよりFirstOrDefault、SingleよりSingleOrDefaultを検討することで例外を避けられる場合があります。

C#
string? name = names.FirstOrDefault(n => n.StartsWith("D"));

if (name != null)
{
Console.WriteLine(name);
}
else
{
Console.WriteLine("該当する名前はありません。");
}

6-4. nullチェックと例外回避

NullReferenceExceptionを防ぐには、nullチェックを習慣化することが大切です。

C#
void PrintUserName(User? user)
{
if (user == null)
{
Console.WriteLine("ユーザー情報がありません。");
return;
}

Console.WriteLine(user.Name);
}

null条件演算子やnull合体演算子も便利です。

C#
string displayName = user?.Name ?? "ゲスト";
Console.WriteLine(displayName);

また、メソッドの引数としてnullを許可しない場合は、ArgumentNullExceptionを投げる方法もあります。

C#
void SendMessage(string message)
{
ArgumentNullException.ThrowIfNull(message);

Console.WriteLine(message);
}

nullを許可するのか、許可しないのかを明確にすることで、例外の発生を減らせます。

6-5. ユーザー入力に対する例外処理

ユーザー入力は、開発者の想定どおりになるとは限りません。空文字、範囲外の数値、不正な形式などを考慮する必要があります。

C#
Console.Write("点数を入力してください: ");
string input = Console.ReadLine();

if (!int.TryParse(input, out int score))
{
Console.WriteLine("数値を入力してください。");
return;
}

if (score < 0 || score > 100)
{
Console.WriteLine("点数は0から100の範囲で入力してください。");
return;
}

Console.WriteLine($"入力された点数: {score}");

このように、入力チェックを段階的に行うと、例外に頼らず安全に処理できます。

ユーザー入力のエラーは、例外というより通常起こり得る入力ミスとして扱うほうが自然です。try-catchは、予期しない問題や外部要因による失敗に使うのが基本です。

7. 非同期処理・LINQ・API通信における例外処理

7-1. async/awaitで発生する例外の扱い方

C#のasync/awaitでも、基本的には通常のtry-catchと同じように例外を処理できます。

C#
try
{
string result = await GetDataAsync();
Console.WriteLine(result);
}
catch (Exception ex)
{
Console.WriteLine("非同期処理でエラーが発生しました。");
Console.WriteLine(ex.Message);
}

awaitしている非同期メソッド内で例外が発生した場合、その例外はawaitの位置で再スローされます。そのため、awaitをtryブロック内に入れることでcatchできます。

C#
async Task<string> GetDataAsync()
{
await Task.Delay(1000);
throw new InvalidOperationException("データ取得に失敗しました。");
}

非同期処理では、例外がどこで発生したのかを見失いやすいため、ログにメソッド名や処理内容を残すことが重要です。

7-2. Taskの例外をcatchする方法

Taskで発生した例外は、awaitすることでcatchできます。

C#
try
{
Task task = Task.Run(() =>
{
throw new InvalidOperationException("Task内で例外が発生しました。");
});

await task;
}
catch (InvalidOperationException ex)
{
Console.WriteLine(ex.Message);
}

一方、awaitせずにTaskを放置すると、例外の扱いが難しくなります。

C#
Task.Run(() =>
{
throw new Exception("例外が発生しました。");
});

このような書き方では、呼び出し元で例外を適切に処理しにくくなります。非同期処理を開始したら、必要に応じてawaitし、例外を捕捉できる形にすることが大切です。

複数のTaskを扱う場合は、Task.WhenAllをtry-catchで囲みます。

C#
try
{
await Task.WhenAll(Task1Async(), Task2Async());
}
catch (Exception ex)
{
Console.WriteLine("いずれかの非同期処理でエラーが発生しました。");
Console.WriteLine(ex.Message);
}

7-3. LINQで起こりやすい例外

LINQでは、First、Single、Max、Minなどのメソッドで例外が発生することがあります。

C#
List<int> numbers = new List<int>();

int first = numbers.First();

空のコレクションに対してFirstを呼ぶとInvalidOperationExceptionが発生します。対処法としてFirstOrDefaultを使います。

C#
int first = numbers.FirstOrDefault();

ただし、intのFirstOrDefaultは該当なしの場合に0を返します。0が有効な値である場合は、該当なしとの区別が必要です。

Singleも注意が必要です。Singleは、条件に一致する要素が1つだけであることを期待します。0件でも複数件でも例外になります。

C#
var user = users.Single(u => u.Id == targetId);

1件とは限らない場合は、SingleOrDefaultやFirstOrDefaultを検討します。

C#
var user = users.SingleOrDefault(u => u.Id == targetId);

if (user == null)
{
Console.WriteLine("ユーザーが見つかりません。");
}

LINQは便利ですが、メソッドごとの例外発生条件を理解して使うことが重要です。

7-4. HttpClient使用時の例外処理

HttpClientを使ってAPI通信を行う場合、ネットワークエラー、タイムアウト、DNSエラー、HTTPステータスエラーなどを考慮する必要があります。

C#
using HttpClient client = new HttpClient();

try
{
HttpResponseMessage response = await client.GetAsync("https://example.com/api/users");
response.EnsureSuccessStatusCode();

string json = await response.Content.ReadAsStringAsync();
Console.WriteLine(json);
}
catch (HttpRequestException ex)
{
Console.WriteLine("HTTP通信でエラーが発生しました。");
Console.WriteLine(ex.Message);
}
catch (TaskCanceledException ex)
{
Console.WriteLine("通信がタイムアウトした可能性があります。");
Console.WriteLine(ex.Message);
}

EnsureSuccessStatusCodeを呼び出すと、成功ステータスではない場合に例外が発生します。APIの仕様によっては、ステータスコードを見て個別に処理するほうが適している場合もあります。

C#
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
Console.WriteLine("データが見つかりませんでした。");
}

7-5. タイムアウトや通信失敗への対処法

API通信では、タイムアウト設定が重要です。応答が返らない状態を無期限に待つと、アプリケーションの応答性が悪くなります。

C#
using HttpClient client = new HttpClient
{
Timeout = TimeSpan.FromSeconds(10)
};

タイムアウトが発生した場合は、ユーザーに再試行を促す、一定回数リトライする、代替データを表示するなどの対応を検討します。

C#
for (int i = 0; i < 3; i++)
{
try
{
HttpResponseMessage response = await client.GetAsync("https://example.com/api");
response.EnsureSuccessStatusCode();

Console.WriteLine("通信に成功しました。");
break;
}
catch (HttpRequestException)
{
Console.WriteLine("通信に失敗しました。再試行します。");
}
catch (TaskCanceledException)
{
Console.WriteLine("通信がタイムアウトしました。再試行します。");
}
}

ただし、むやみにリトライするとサーバーに負荷をかけることがあります。リトライ回数、待機時間、対象とする例外を慎重に決めることが大切です。

8. 独自例外クラスの作り方

8-1. 独自例外を作るメリット

C#では、Exceptionクラスを継承して独自例外クラスを作れます。独自例外を使うと、アプリケーション固有のエラーを明確に表現できます。

たとえば、在庫不足、権限不足、業務ルール違反などは、標準の例外だけでは意味が伝わりにくい場合があります。そのようなときに独自例外を作ると、呼び出し元でエラーの種類を判断しやすくなります。

C#
try
{
Order();
}
catch (StockShortageException)
{
Console.WriteLine("在庫が不足しています。");
}

このように、例外型を見るだけで何が起きたのかがわかりやすくなります。

8-2. Exceptionクラスを継承する方法

独自例外クラスは、Exceptionを継承して作成します。

C#
public class StockShortageException : Exception
{
public StockShortageException()
{
}

public StockShortageException(string message)
: base(message)
{
}

public StockShortageException(string message, Exception innerException)
: base(message, innerException)
{
}
}

一般的には、引数なしのコンストラクター、メッセージを受け取るコンストラクター、InnerExceptionを受け取るコンストラクターを用意します。

8-3. 独自例外をthrowする実装例

独自例外を使った実装例を見てみましょう。

C#
public class StockShortageException : Exception
{
public StockShortageException(string message)
: base(message)
{
}
}

public class Product
{
public int Stock { get; set; }
}

public void Purchase(Product product, int quantity)
{
if (product == null)
{
throw new ArgumentNullException(nameof(product));
}

if (quantity <= 0)
{
throw new ArgumentException("購入数は1以上である必要があります。", nameof(quantity));
}

if (product.Stock < quantity)
{
throw new StockShortageException("在庫が不足しています。");
}

product.Stock -= quantity;
}

呼び出し側では、独自例外をcatchして個別に処理できます。

C#
try
{
Purchase(product, 10);
}
catch (StockShortageException ex)
{
Console.WriteLine(ex.Message);
}
catch (ArgumentException ex)
{
Console.WriteLine("入力値が不正です。");
Console.WriteLine(ex.Message);
}

8-4. 独自例外を使うべき場面

独自例外は、標準の例外型では意味が伝わりにくく、呼び出し元で個別に扱う必要がある場合に使います。

たとえば、次のような場面です。

・業務ルール違反を明確に表したい
・特定のエラーだけをcatchしたい
・アプリケーション固有の失敗理由を表現したい
・エラー情報に独自のプロパティを持たせたい

一方、引数がnullである、数値の形式が不正である、ファイルが存在しないといった一般的な問題には、標準例外を使うほうが自然です。

8-5. 独自例外を乱用しないための注意点

独自例外は便利ですが、作りすぎるとコードが複雑になります。例外型が多すぎると、どれを使うべきか判断しにくくなり、catchの設計も難しくなります。

独自例外を作る前に、まず標準例外で表現できないかを考えましょう。ArgumentException、InvalidOperationException、IOExceptionなどで十分な場合も多くあります。

また、単にメッセージを変えたいだけなら、独自例外を作らず標準例外に適切なメッセージを渡すだけで十分なこともあります。

C#
throw new InvalidOperationException("注文はすでに確定済みのため変更できません。");

独自例外は、呼び出し元がその例外型を区別して処理する価値がある場合に使うのが基本です。

9. C#の例外処理でやってはいけないこと

9-1. catchで例外を握りつぶす

最も避けたい例外処理の一つが、catchで何もしないことです。

C#
try
{
SaveData();
}
catch
{
}

このようなコードでは、例外が発生しても何も記録されず、処理が失敗したことすらわからなくなります。障害が起きたときに原因を追跡できません。

最低限、ログを残す、ユーザーに通知する、必要であれば再スローするなどの対応が必要です。

C#
try
{
SaveData();
}
catch (Exception ex)
{
Console.WriteLine("保存処理に失敗しました。");
Console.WriteLine(ex.Message);
throw;
}

例外を握りつぶすと、不具合の発見が遅れ、データ不整合につながる可能性があります。

9-2. Exceptionだけで何でもcatchする

Exceptionでまとめてcatchすること自体が常に悪いわけではありません。しかし、すべてをExceptionだけで処理すると、例外の種類に応じた適切な対応ができなくなります。

C#
try
{
Process();
}
catch (Exception)
{
Console.WriteLine("エラーが発生しました。");
}

この書き方では、ファイルがないのか、入力が不正なのか、通信に失敗したのかがわかりません。

できるだけ具体的な例外型をcatchしましょう。

C#
try
{
Process();
}
catch (FileNotFoundException)
{
Console.WriteLine("ファイルが見つかりません。");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("アクセス権限がありません。");
}
catch (Exception ex)
{
Console.WriteLine("予期しないエラーが発生しました。");
Console.WriteLine(ex.Message);
}

最後にExceptionを置く場合でも、ログを残すなどの対応を行うことが重要です。

9-3. 例外を通常の分岐処理に使う

例外は、通常の条件分岐の代わりに使うべきではありません。

たとえば、ユーザー入力が数値かどうかを判定するために毎回int.Parseで例外を発生させるのは適切ではありません。

C#
try
{
int number = int.Parse(input);
}
catch
{
Console.WriteLine("数値ではありません。");
}

ユーザーが不正な値を入力することは十分に予測できます。この場合はTryParseを使うほうが適切です。

C#
if (int.TryParse(input, out int number))
{
Console.WriteLine(number);
}
else
{
Console.WriteLine("数値ではありません。");
}

例外は、予測できる通常の分岐ではなく、通常の処理では継続できない異常な状態を表すために使いましょう。

9-4. エラーログを残さない

例外が発生したときにログを残さないと、後から原因を調査できません。特に本番環境では、ユーザーから「動かない」と報告されても、ログがなければ何が起きたのかわからないことがあります。

ログに残すべき情報には、次のようなものがあります。

・例外の種類
・エラーメッセージ
・スタックトレース
・発生日時
・処理内容
・入力値や対象データの識別子
・InnerExceptionの情報

ただし、パスワード、トークン、クレジットカード番号、個人情報などの機密情報をログに出力してはいけません。

9-5. ユーザーに詳細な内部エラーを表示する

例外メッセージやスタックトレースをそのままユーザーに表示するのは危険です。内部構造、ファイルパス、SQL、API情報などが漏れる可能性があります。

悪い例は以下です。

C#
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}

開発中は役立つこともありますが、本番環境でユーザーに見せるべきではありません。

ユーザーにはわかりやすい一般的なメッセージを表示し、詳細はログに残します。

C#
catch (Exception ex)
{
logger.LogError(ex, "注文処理に失敗しました。");
Console.WriteLine("処理中に問題が発生しました。時間をおいて再度お試しください。");
}

ユーザー向けメッセージと開発者向けログは分けて考えることが大切です。

10. C#の例外処理のベストプラクティス

10-1. 予測できるエラーは事前チェックで防ぐ

予測できる問題は、例外が発生する前にチェックしましょう。

たとえば、null、空文字、範囲外の数値、不正な入力形式などは事前に確認できます。

C#
if (string.IsNullOrWhiteSpace(name))
{
Console.WriteLine("名前を入力してください。");
return;
}

数値変換であればTryParseを使います。

C#
if (!int.TryParse(input, out int number))
{
Console.WriteLine("数値を入力してください。");
return;
}

例外処理に頼りすぎず、入力チェックや状態確認で防げるものは防ぐことが、安定したC#コードを書くコツです。

10-2. 必要な範囲だけtry-catchする

try-catchの範囲が広すぎると、どの処理で例外が発生したのかわかりにくくなります。

C#
try
{
Validate();
ReadFile();
ConvertData();
SaveData();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}

このように広い範囲をまとめてcatchすると、どこで失敗したのか判断しづらくなります。

必要に応じて、例外が発生しやすい処理だけをtry-catchで囲みます。

C#
try
{
string content = File.ReadAllText(path);
Console.WriteLine(content);
}
catch (IOException ex)
{
Console.WriteLine("ファイル読み込みに失敗しました。");
Console.WriteLine(ex.Message);
}

try-catchは必要な範囲に絞ることで、コードの意図が明確になります。

10-3. 具体的な例外型をcatchする

例外処理では、できるだけ具体的な例外型をcatchします。

C#
try
{
string content = File.ReadAllText(path);
}
catch (FileNotFoundException)
{
Console.WriteLine("ファイルが見つかりません。");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("アクセス権限がありません。");
}

具体的な例外型を使うと、原因に応じた適切な対応ができます。

最後に予期しない例外を扱うためにExceptionをcatchすることもありますが、その場合は必ずログを残すなど、調査できる形にしましょう。

C#
catch (Exception ex)
{
logger.LogError(ex, "予期しないエラーが発生しました。");
throw;
}

10-4. ログに残すべき情報

例外発生時のログは、障害調査のために非常に重要です。ログには、単なるメッセージだけでなく、原因を追える情報を残す必要があります。

C#
try
{
ProcessOrder(orderId);
}
catch (Exception ex)
{
logger.LogError(ex, "注文処理に失敗しました。OrderId: {OrderId}", orderId);
throw;
}

ログに残すべき情報は、例外型、メッセージ、スタックトレース、処理対象のID、実行中の操作、発生日時などです。

一方で、個人情報や認証情報をログに残さないよう注意が必要です。ログは開発者や運用担当者が見るものですが、扱い方を誤ると情報漏えいの原因になります。

10-5. 保守しやすい例外処理を書くコツ

保守しやすい例外処理を書くには、例外処理の役割を明確にすることが大切です。

具体的には、次のような点を意識します。

・例外を握りつぶさない
・具体的な例外型をcatchする
・ユーザー向けメッセージとログを分ける
・throwでは元のスタックトレースを壊さない
・予測できる入力ミスは事前チェックで防ぐ
・例外処理を広げすぎない
・独自例外は必要な場合だけ使う

例外処理は、書けば書くほど良いわけではありません。むしろ、過剰なtry-catchはコードを読みにくくします。

重要なのは、「どこで対処すべき例外なのか」「呼び出し元に任せるべき例外なのか」を判断することです。低レベルの処理では例外をそのまま投げ、アプリケーションの境界部分でログやユーザー通知を行う設計がよく使われます。

11. C#の例外処理に関するよくある質問

11-1. try-catchはどこに書くべき?

try-catchは、例外が発生する可能性があり、その場で適切に対処できる場所に書きます。

たとえば、ファイル読み込みに失敗した場合に代替ファイルを使う、API通信に失敗した場合にリトライする、ユーザーにエラーメッセージを表示する、といった対応ができる場所です。

逆に、その場で何もできないのにcatchするのは避けましょう。ログを残すだけならアプリケーションの上位層でまとめて処理したほうがよい場合もあります。

11-2. catchしない例外はどうなる?

catchされなかった例外は、呼び出し元へ伝播します。呼び出し元にもcatchがなければ、さらに上の呼び出し元へ伝わります。最終的にどこにもcatchされない場合、アプリケーションが異常終了することがあります。

C#
void A()
{
B();
}

void B()
{
throw new Exception("例外が発生しました。");
}

この例では、Bで発生した例外がAへ伝わります。Aでもcatchされなければ、さらにその呼び出し元へ伝わります。

すべての例外をその場でcatchする必要はありませんが、アプリケーション全体としてどこで例外を処理するのかを設計しておくことが大切です。

11-3. 例外処理とif文はどう使い分ける?

予測できる条件分岐にはif文を使い、通常の処理を続けられない異常な状態には例外を使います。

たとえば、ユーザーが空文字を入力する可能性は十分に予測できます。この場合はif文でチェックします。

C#
if (string.IsNullOrWhiteSpace(input))
{
Console.WriteLine("入力してください。");
return;
}

一方、ファイル読み込み中に突然アクセスできなくなる、ネットワークが切断される、外部APIが失敗するなどは例外処理で扱うことが多いです。

判断の目安は、「通常の流れとして起こり得るか」「その場で条件判定できるか」です。通常起こり得るものはif文、予期しにくい失敗や処理継続不能な状態は例外処理と考えるとわかりやすいです。

11-4. finallyは必ず実行される?

finallyは、tryブロック内で例外が発生してもしなくても、通常は実行されます。catchで例外を処理した場合も、catchせずに例外が呼び出し元へ伝播する場合も、finallyは実行されます。

C#
try
{
Console.WriteLine("try");
throw new Exception();
}
finally
{
Console.WriteLine("finally");
}

このコードでは、例外が発生してもfinallyが実行されます。

ただし、プロセスが強制終了された場合や、環境そのものが停止した場合など、特殊なケースでは実行されないことがあります。通常のC#プログラムでは、後片付けが必要な処理をfinallyに書くと考えて問題ありません。

11-5. 初心者がまず覚えるべき例外処理は?

C#初心者がまず覚えるべきなのは、try-catchの基本、具体的な例外型のcatch、例外メッセージとスタックトレースの確認、そしてnullや入力値の事前チェックです。

最初は、以下の形を理解することから始めるとよいでしょう。

C#
try
{
int number = int.Parse("abc");
}
catch (FormatException ex)
{
Console.WriteLine("数値の形式が正しくありません。");
Console.WriteLine(ex.Message);
}

次に、よく発生するNullReferenceException、IndexOutOfRangeException、FormatException、IOExceptionを覚えると、実際の開発でエラー原因を見つけやすくなります。

また、例外が出たときは慌てず、次の順番で確認しましょう。

・例外の種類
・エラーメッセージ
・発生した行
・変数の値
・スタックトレース

これだけでも、C#の例外対応力は大きく向上します。

まとめ

C#の例外は、プログラムの実行中に発生した問題を通知し、安全に対処するための重要な仕組みです。例外処理を理解しておくと、アプリケーションの異常終了を防ぎ、原因調査をしやすくし、ユーザーにわかりやすいエラーメッセージを表示できます。

基本となるのはtry-catchです。例外が発生する可能性のある処理をtryに書き、発生した例外をcatchで受け取ります。複数のcatchを使う場合は、具体的な例外型から順に書くことが重要です。

また、finallyは後片付けに使い、throwは例外を明示的に発生させるときに使います。リソース解放にはusing文を活用すると、安全で読みやすいコードになります。

実践では、NullReferenceException、IndexOutOfRangeException、FormatException、IOExceptionなどがよく発生します。これらは、nullチェック、範囲チェック、TryParse、適切なファイル操作の例外処理によって防げることも多いです。

例外処理で大切なのは、何でもcatchすることではありません。予測できるエラーは事前チェックで防ぎ、必要な範囲だけtry-catchし、具体的な例外型を扱い、ログを残すことです。

C#の例外処理を正しく使えるようになると、コードの信頼性と保守性が大きく向上します。エラーが起きたときに原因を追える設計を意識しながら、読みやすく安全な例外処理を書いていきましょう。