C#例外処理の基本と使い方|try-catch・throw・finallyを初心者向けに徹底解説

はじめに

C#でアプリケーションを開発していると、文字列を数値に変換できない、ファイルが見つからない、nullの値を参照してしまうなど、実行中にさまざまな問題が発生します。このような実行時の問題に対応するために使う仕組みが「例外処理」です。

C#の例外処理では、主にtrycatchfinallythrowを使います。これらを正しく理解しておくと、プログラムが突然停止するのを防いだり、エラーの原因を調査しやすくしたり、ユーザーに分かりやすいメッセージを表示したりできます。

この記事では、C# 例外処理の基本から、try-catchの使い方、throwによる例外の発生、finallyによる後処理、よく使う例外クラス、独自例外、非同期処理での注意点まで、初心者にも分かりやすく解説します。

1. C#の例外処理とは?まず押さえる基本

1-1. 例外処理の意味と役割

例外処理とは、プログラムの実行中に発生した異常な状態を検知し、適切に対応するための仕組みです。

たとえば、次のような状況が起きることがあります。

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

"abc"は数値に変換できないため、このコードを実行すると例外が発生します。例外処理を書いていない場合、プログラムはそこで停止してしまいます。

そこで、C#ではtry-catchを使って例外を受け取り、エラー時の処理を記述します。

C#
try
{
int number = int.Parse("abc");
}
catch (FormatException)
{
Console.WriteLine("数値に変換できませんでした。");
}

このように例外処理を書くことで、問題が発生してもプログラム全体を終了させず、適切なメッセージ表示や代替処理を行えます。

1-2. 例外が発生する代表的なケース

C#で例外が発生する代表的なケースには、次のようなものがあります。

文字列を数値に変換できない場合、FormatExceptionが発生します。

C#
int value = int.Parse("abc");

0で割り算をした場合、DivideByZeroExceptionが発生します。

C#
int result = 10 / 0;

nullの変数からメンバーにアクセスした場合、NullReferenceExceptionが発生します。

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

存在しないファイルを読み込もうとした場合、FileNotFoundExceptionIOExceptionが発生することがあります。

C#
string content = File.ReadAllText("notfound.txt");

配列の範囲外にアクセスした場合、IndexOutOfRangeExceptionが発生します。

C#
int[] numbers = { 1, 2, 3 };
Console.WriteLine(numbers[5]);

これらはC# 例外処理を学ぶうえでよく出てくる基本的な例外です。

1-3. エラー・例外・バグの違い

初心者が混同しやすい言葉に、「エラー」「例外」「バグ」があります。

エラーは、プログラムに問題がある状態全般を指す広い言葉です。コンパイルエラー、実行時エラー、論理エラーなどがあります。

例外は、プログラムの実行中に発生する異常な状態を表すものです。C#では例外がオブジェクトとして扱われ、catchで受け取ることができます。

バグは、開発者が意図していない動作をする原因となるプログラム上の欠陥です。たとえば、計算式の間違いや条件分岐のミスはバグです。

つまり、例外は実行時に起きる問題の一種であり、バグが原因で例外が発生することもあります。ただし、すべての例外がバグとは限りません。ユーザーが不正な入力をしたり、外部ファイルが削除されていたりする場合も例外が発生します。

1-4. 例外処理が必要な理由

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

例外処理があると、異常が発生したときに次のような対応ができます。

ユーザーに分かりやすいメッセージを表示できます。

C#
Console.WriteLine("入力内容を確認してください。");

ログを残して、あとから原因を調査できます。

C#
Console.WriteLine(ex.Message);

ファイルやデータベース接続などのリソースを確実に解放できます。

C#
finally
{
Console.WriteLine("後処理を実行します。");
}

処理を中断するか、呼び出し元に例外を伝えるかを制御できます。

例外処理は、単にエラーを隠すためのものではありません。問題が起きたときに、適切に検知し、安全に処理を継続または終了するための重要な仕組みです。

1-5. 例外処理を書かないとどうなるか

例外処理を書かない場合、例外が発生した時点でプログラムが異常終了する可能性があります。

たとえば、次のコードを見てみましょう。

C#
Console.WriteLine("開始");

int number = int.Parse("abc");

Console.WriteLine("終了");

この場合、int.Parse("abc")FormatExceptionが発生します。そのため、後続のConsole.WriteLine("終了")は実行されません。

例外処理を書くと、異常が発生しても処理を制御できます。

C#
Console.WriteLine("開始");

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

Console.WriteLine("終了");

このコードでは、例外が発生してもcatchブロックで処理されるため、最後の「終了」まで実行されます。

2. C#のtry-catchの基本構文と使い方

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

C#の例外処理で最も基本となるのがtry-catchです。

基本構文は次のとおりです。

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

tryブロックには、例外が発生する可能性のある処理を書きます。catchブロックには、例外が発生したときに実行したい処理を書きます。

たとえば、数値変換の例外処理は次のように書けます。

C#
try
{
int number = int.Parse("123");
Console.WriteLine(number);
}
catch (Exception ex)
{
Console.WriteLine("エラーが発生しました。");
Console.WriteLine(ex.Message);
}

2-2. tryブロックに書く処理

tryブロックには、失敗する可能性のある処理を書きます。

代表的な例は次のとおりです。

C#
try
{
string input = Console.ReadLine();
int number = int.Parse(input);
Console.WriteLine(number);
}
catch (FormatException)
{
Console.WriteLine("数値を入力してください。");
}

この例では、ユーザーが数値以外を入力するとFormatExceptionが発生します。例外が発生すると、tryブロック内の残りの処理はスキップされ、対応するcatchブロックに移動します。

tryブロックには何でも入れられますが、範囲を広げすぎると、どの処理で例外が発生したのか分かりにくくなります。基本的には、例外が発生する可能性がある処理を中心に書くのがよいでしょう。

2-3. catchブロックで例外を受け取る方法

catchブロックでは、発生した例外を受け取ることができます。

C#
try
{
int number = int.Parse("abc");
}
catch (FormatException ex)
{
Console.WriteLine("変換に失敗しました。");
Console.WriteLine(ex.Message);
}

exには例外の情報が入っています。よく使うプロパティには、次のようなものがあります。

Messageは、例外の内容を表すメッセージです。

C#
Console.WriteLine(ex.Message);

StackTraceは、例外がどこで発生したかを追跡する情報です。

C#
Console.WriteLine(ex.StackTrace);

InnerExceptionは、例外の原因となった内側の例外を表します。

C#
Console.WriteLine(ex.InnerException?.Message);

初心者のうちは、まずMessageを確認するところから始めると理解しやすいです。

2-4. Exceptionクラスを使った基本例

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

C#
try
{
int number = int.Parse("abc");
}
catch (Exception ex)
{
Console.WriteLine("例外が発生しました。");
Console.WriteLine(ex.Message);
}

ただし、何でもExceptionでcatchすればよいわけではありません。Exceptionで広く受け取ると、想定外の重大な問題まで握りつぶしてしまう可能性があります。

基本的には、想定できる例外を具体的な例外クラスでcatchし、必要な場合だけ最後にExceptionを使うのがよいでしょう。

C#
try
{
int number = int.Parse("abc");
}
catch (FormatException)
{
Console.WriteLine("数値の形式が正しくありません。");
}
catch (Exception ex)
{
Console.WriteLine("予期しないエラーが発生しました。");
Console.WriteLine(ex.Message);
}

2-5. try-catchの実行順序

try-catchの実行順序は次のようになります。

まず、tryブロックの処理が上から順に実行されます。

C#
try
{
Console.WriteLine("1");
int number = int.Parse("abc");
Console.WriteLine("2");
}
catch (FormatException)
{
Console.WriteLine("3");
}

このコードでは、まず「1」が表示されます。次にint.Parse("abc")で例外が発生します。その時点でtryブロックの残りの処理は実行されないため、「2」は表示されません。そしてcatchブロックに移動し、「3」が表示されます。

つまり、出力結果は次のようになります。

1
3

例外が発生しなかった場合は、catchブロックは実行されません。

C#
try
{
int number = int.Parse("123");
Console.WriteLine(number);
}
catch (FormatException)
{
Console.WriteLine("変換できませんでした。");
}

この場合、数値変換に成功するため、catchは実行されません。

2-6. 初心者向けサンプルコードで理解する

次のサンプルは、ユーザー入力を数値に変換し、失敗した場合に例外処理を行う例です。

C#
Console.WriteLine("年齢を入力してください。");

try
{
string input = Console.ReadLine();
int age = int.Parse(input);

Console.WriteLine($"あなたの年齢は{age}歳です。");
}
catch (FormatException)
{
Console.WriteLine("年齢は数値で入力してください。");
}
catch (Exception ex)
{
Console.WriteLine("予期しないエラーが発生しました。");
Console.WriteLine(ex.Message);
}

このコードでは、数値を入力すると正常に年齢が表示されます。一方、abcのような文字を入力するとFormatExceptionが発生し、「年齢は数値で入力してください。」と表示されます。

C# 例外処理の基本は、このように「失敗する可能性がある処理をtryに書き、失敗したときの処理をcatchに書く」と覚えると分かりやすいです。

3. catchの書き方と複数例外の扱い方

3-1. 例外の種類ごとに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);
}

このように例外の種類ごとにcatchを分けると、原因に応じて適切なメッセージや処理を実行できます。

3-2. FormatExceptionの使い方

FormatExceptionは、文字列の形式が正しくないときによく発生します。

代表的なのは、文字列を数値や日付に変換する処理です。

C#
try
{
int number = int.Parse("abc");
}
catch (FormatException)
{
Console.WriteLine("数値として正しい形式ではありません。");
}

日付変換でも発生することがあります。

C#
try
{
DateTime date = DateTime.Parse("not date");
}
catch (FormatException)
{
Console.WriteLine("日付の形式が正しくありません。");
}

ただし、ユーザー入力の数値変換では、例外処理よりもTryParseを使う方が適している場合もあります。

C#
string input = "abc";

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

入力ミスのように事前に想定できるケースでは、例外ではなくTryParseで判定するのが一般的です。

3-3. DivideByZeroExceptionの使い方

DivideByZeroExceptionは、整数の割り算で0除算を行ったときに発生します。

C#
try
{
int x = 10;
int y = 0;

int result = x / y;
Console.WriteLine(result);
}
catch (DivideByZeroException)
{
Console.WriteLine("0で割ることはできません。");
}

0で割る可能性がある場合は、例外処理だけでなく、事前チェックを行うことも大切です。

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

if (y == 0)
{
Console.WriteLine("割る数に0は指定できません。");
}
else
{
int result = x / y;
Console.WriteLine(result);
}

例外は本来、通常の分岐処理の代わりに使うものではありません。0が入力される可能性が高いなら、if文でチェックした方が読みやすく、安全です。

3-4. NullReferenceExceptionの考え方

NullReferenceExceptionは、nullのオブジェクトに対してメンバーへアクセスしたときに発生します。

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

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

NullReferenceExceptionは、catchして対応するよりも、発生しないように設計することが重要です。

C#
string name = null;

if (name != null)
{
Console.WriteLine(name.Length);
}
else
{
Console.WriteLine("名前が設定されていません。");
}

また、null条件演算子を使うこともできます。

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

null合体演算子を使って初期値を指定する方法もあります。

C#
string name = null;
string displayName = name ?? "未設定";

Console.WriteLine(displayName);

C# 例外処理では、NullReferenceExceptionをcatchするより、nullを事前に防ぐ設計が基本です。

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

複数のcatchを書く場合、順番に注意が必要です。

C#では、上から順にcatchが判定されます。そのため、親クラスであるExceptionを先に書くと、後ろの具体的な例外クラスに到達できません。

悪い例は次のとおりです。

C#
try
{
int number = int.Parse("abc");
}
catch (Exception)
{
Console.WriteLine("エラーが発生しました。");
}
catch (FormatException)
{
Console.WriteLine("数値の形式が正しくありません。");
}

このような書き方は、FormatExceptionExceptionに含まれるため適切ではありません。

正しくは、具体的な例外を先に書き、最後に広い例外を書くことです。

C#
try
{
int number = int.Parse("abc");
}
catch (FormatException)
{
Console.WriteLine("数値の形式が正しくありません。");
}
catch (Exception)
{
Console.WriteLine("予期しないエラーが発生しました。");
}

catchの順番は「具体的な例外から、一般的な例外へ」と覚えておきましょう。

3-6. catch whenによる条件付き例外処理

C#では、catch whenを使って条件付きで例外を処理できます。

C#
try
{
int number = int.Parse("abc");
}
catch (FormatException ex) when (ex.Message.Contains("input"))
{
Console.WriteLine("入力形式に関するエラーです。");
}
catch (FormatException)
{
Console.WriteLine("数値形式が正しくありません。");
}

catch whenは、同じ例外クラスでも条件によって処理を分けたい場合に便利です。

たとえば、業務システムでエラーコードによって処理を分ける場合にも使えます。

C#
try
{
throw new InvalidOperationException("在庫不足");
}
catch (InvalidOperationException ex) when (ex.Message == "在庫不足")
{
Console.WriteLine("在庫が不足しています。");
}
catch (InvalidOperationException)
{
Console.WriteLine("操作を実行できません。");
}

ただし、条件が複雑になりすぎると読みづらくなるため、必要な場面に絞って使いましょう。

4. finallyの基本と確実に実行したい処理の書き方

4-1. finallyとは何か

finallyは、例外が発生してもしなくても最後に実行されるブロックです。

基本構文は次のとおりです。

C#
try
{
// 例外が発生する可能性のある処理
}
catch (Exception ex)
{
// 例外が発生したときの処理
}
finally
{
// 最後に必ず実行したい処理
}

finallyは、主に後処理を書くために使います。たとえば、ファイルを閉じる、データベース接続を閉じる、一時的に確保したリソースを解放する、といった処理です。

4-2. finallyが実行されるタイミング

finallyは、例外が発生した場合も、発生しなかった場合も実行されます。

C#
try
{
Console.WriteLine("try");
}
catch
{
Console.WriteLine("catch");
}
finally
{
Console.WriteLine("finally");
}

例外が発生しない場合、出力は次のようになります。

try
finally

例外が発生した場合は、次のようになります。

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

出力結果は次のとおりです。

try
catch
finally

このように、finallyは最後に実行される後処理用のブロックです。

4-3. ファイル・DB・リソース解放で使う例

finallyは、ファイルやデータベース接続など、使い終わったら解放が必要なリソースに対してよく使われます。

C#
StreamReader reader = null;

try
{
reader = new StreamReader("sample.txt");
string content = reader.ReadToEnd();

Console.WriteLine(content);
}
catch (IOException ex)
{
Console.WriteLine("ファイルの読み込みに失敗しました。");
Console.WriteLine(ex.Message);
}
finally
{
if (reader != null)
{
reader.Dispose();
}
}

この例では、ファイル読み込み中に例外が発生しても、finallyDisposeが呼ばれます。これにより、ファイルリソースを確実に解放できます。

ただし、現在のC#では、リソース解放にはusing文やusing宣言を使うことが多いです。

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

Console.WriteLine(content);

4-4. finallyとreturnの関係

trycatchの中でreturnした場合でも、finallyは実行されます。

C#
static int GetNumber()
{
try
{
return 1;
}
finally
{
Console.WriteLine("finallyが実行されました。");
}
}

このコードでは、return 1;が実行される前に、finallyの処理が実行されます。

ただし、finallyの中でreturnを書くのは避けるべきです。

C#
static int GetNumber()
{
try
{
return 1;
}
finally
{
return 2;
}
}

このようなコードは、tryreturnを上書きしてしまうため、処理の流れが分かりにくくなります。finallyには、戻り値を変更するような処理ではなく、後処理を書くのが基本です。

4-5. finallyを使うべき場面と使わない場面

finallyを使うべき場面は、成功しても失敗しても必ず実行したい後処理がある場合です。

たとえば、次のようなケースです。

ファイルを閉じる処理。

データベース接続を閉じる処理。

一時ファイルを削除する処理。

ロックを解除する処理。

状態を元に戻す処理。

一方で、単にメッセージを表示するだけの場合や、リソース解放がusingで十分な場合は、無理にfinallyを書く必要はありません。

C#
using var reader = new StreamReader("sample.txt");
string content = reader.ReadToEnd();

このように書けば、スコープを抜けるときに自動的にDisposeされます。

4-6. using文・using宣言との違い

finallyusingは、どちらも後処理に関係します。

finallyは、任意の後処理を明示的に書くための構文です。

C#
try
{
// 処理
}
finally
{
// 後処理
}

一方、usingは、IDisposableを実装したオブジェクトを自動的に破棄するための構文です。

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

using宣言を使うと、さらにシンプルに書けます。

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

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

ファイル、ストリーム、データベース接続などのリソース解放には、finallyよりもusingを使う方が簡潔で安全です。ただし、リソース解放以外の後処理を行いたい場合は、finallyが役立ちます。

5. throwの使い方と例外を発生させる方法

5-1. throwとは何か

throwは、例外を発生させるためのキーワードです。

C#では、問題が発生したことを呼び出し元に知らせたい場合にthrowを使います。

C#
throw new Exception("エラーが発生しました。");

たとえば、メソッドに不正な引数が渡された場合、そのまま処理を続けるのではなく、例外を投げて呼び出し元に知らせることがあります。

5-2. throw new Exceptionの基本構文

例外を新しく発生させる基本構文は次のとおりです。

C#
throw new Exception("例外メッセージ");

実際の例を見てみましょう。

C#
static void CheckAge(int age)
{
if (age < 0)
{
throw new Exception("年齢に負の値は指定できません。");
}

Console.WriteLine($"年齢は{age}歳です。");
}

このメソッドに負の値を渡すと、例外が発生します。

C#
try
{
CheckAge(-1);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}

ただし、実務ではthrow new Exceptionよりも、状況に合った具体的な例外クラスを使う方が望ましいです。

5-3. 引数チェックで例外を投げる例

引数が不正な場合は、ArgumentExceptionArgumentNullExceptionを使います。

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

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

nullが許可されない引数には、ArgumentNullExceptionを使います。

C#
static void PrintName(string name)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}

Console.WriteLine(name);
}

引数チェックで例外を投げると、メソッドの利用者に「この値は不正である」と明確に伝えられます。

5-4. throwとthrow exの違い

catchした例外を再度投げる場合、throwthrow exには重要な違いがあります。

正しい再スローは次のように書きます。

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

一方、次のようなthrow exは避けるべきです。

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

throw exを使うと、例外が最初に発生した場所のスタックトレースが失われる可能性があります。原因調査が難しくなるため、catchした例外をそのまま再スローする場合はthrow;を使いましょう。

5-5. スタックトレースを壊さない再スロー方法

スタックトレースを壊さずに再スローするには、catchブロック内でthrow;を使います。

C#
try
{
Execute();
}
catch (Exception ex)
{
Console.WriteLine("ログに記録します。");
Console.WriteLine(ex.Message);

throw;
}

このようにすると、ログを残したうえで、元の例外情報を保ったまま呼び出し元へ例外を伝えられます。

例外を別の例外で包みたい場合は、InnerExceptionとして元の例外を渡します。

C#
try
{
Execute();
}
catch (Exception ex)
{
throw new InvalidOperationException("処理に失敗しました。", ex);
}

この方法なら、上位層では分かりやすい例外として扱いながら、元の原因も保持できます。

5-6. 例外メッセージの書き方

例外メッセージは、原因が分かる内容にすることが大切です。

悪い例は次のようなメッセージです。

C#
throw new Exception("エラー");

これでは、何が原因で、どの値が問題なのか分かりません。

よりよい例は次のとおりです。

C#
throw new ArgumentException("ユーザー名は空にできません。", nameof(userName));

例外メッセージには、次の情報を含めると分かりやすくなります。

何が問題なのか。

どの引数や値が原因なのか。

呼び出し元がどう修正すべきか。

ただし、パスワードや個人情報などの機密情報を例外メッセージに含めないよう注意しましょう。

6. C#でよく使う例外クラス一覧

6-1. Exception

Exceptionは、C#の例外クラスの基本となるクラスです。

C#
try
{
throw new Exception("基本的な例外です。");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}

すべての例外をまとめてcatchできますが、範囲が広すぎるため、使いどころには注意が必要です。初心者のうちは便利に見えますが、実務では具体的な例外クラスを優先しましょう。

6-2. ArgumentException

ArgumentExceptionは、メソッドに渡された引数が不正な場合に使います。

C#
static void SetAge(int age)
{
if (age < 0)
{
throw new ArgumentException("年齢は0以上で指定してください。", nameof(age));
}
}

「引数は渡されているが、値として不適切」という場合に向いています。

6-3. ArgumentNullException

ArgumentNullExceptionは、nullを許可しない引数にnullが渡された場合に使います。

C#
static void PrintMessage(string message)
{
if (message == null)
{
throw new ArgumentNullException(nameof(message));
}

Console.WriteLine(message);
}

nullチェックでは非常によく使う例外クラスです。

6-4. InvalidOperationException

InvalidOperationExceptionは、現在のオブジェクトの状態では操作を実行できない場合に使います。

C#
class Order
{
public bool IsPaid { get; set; }

public void Ship()
{
if (!IsPaid)
{
throw new InvalidOperationException("支払い前の注文は発送できません。");
}

Console.WriteLine("発送しました。");
}
}

引数の問題ではなく、状態の問題を表す場合に適しています。

6-5. FormatException

FormatExceptionは、文字列の形式が正しくない場合に発生します。

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

数値変換、日付変換などでよく見かける例外です。

6-6. IndexOutOfRangeException

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

C#
int[] numbers = { 1, 2, 3 };

try
{
Console.WriteLine(numbers[10]);
}
catch (IndexOutOfRangeException)
{
Console.WriteLine("配列の範囲外です。");
}

この例外はcatchするより、ループ条件やインデックスの指定を見直して防ぐべきケースが多いです。

6-7. NullReferenceException

NullReferenceExceptionは、nullのオブジェクトにアクセスした場合に発生します。

C#
string text = null;

try
{
Console.WriteLine(text.Length);
}
catch (NullReferenceException)
{
Console.WriteLine("オブジェクトがnullです。");
}

ただし、基本的にはcatchするより、nullチェックやnullable参照型を活用して未然に防ぐことが重要です。

6-8. IOException

IOExceptionは、ファイルやストリームなどの入出力処理で発生する例外です。

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

ファイルが使用中、アクセスできない、読み込みに失敗したといったケースで使われます。

6-9. 例外クラスを選ぶ判断基準

例外クラスを選ぶときは、何が原因で失敗したのかを基準にします。

引数が不正ならArgumentException

引数がnullならArgumentNullException

現在の状態では操作できないならInvalidOperationException

文字列の形式が不正ならFormatException

入出力処理の問題ならIOException

具体的な例外クラスを使うことで、呼び出し元が原因を判断しやすくなります。何でもExceptionにしてしまうと、エラーの意味が曖昧になるため注意しましょう。

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

7-1. 独自例外クラスとは

独自例外クラスとは、自分のアプリケーションや業務ロジックに合わせて作成する例外クラスです。

たとえば、在庫不足、残高不足、権限不足など、標準の例外クラスだけでは意味が伝わりにくい場合に使います。

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

独自例外を使うと、どのような業務エラーなのかがコード上で分かりやすくなります。

7-2. 独自例外を作るべき場面

独自例外を作るべき場面は、アプリケーション固有のエラーを明確に表現したい場合です。

たとえば、ECサイトなら次のような例があります。

在庫が不足している。

注文がキャンセル済みで処理できない。

支払いが完了していない。

ユーザーに操作権限がない。

標準例外のInvalidOperationExceptionでも表現できますが、独自例外を使うとcatch側で処理を分けやすくなります。

C#
try
{
OrderProduct(100);
}
catch (StockShortageException ex)
{
Console.WriteLine(ex.Message);
}

7-3. Exceptionクラスを継承する基本例

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

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

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

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

基本的には、メッセージを受け取るコンストラクターと、内側の例外を受け取るコンストラクターを用意しておくと使いやすくなります。

使用例は次のとおりです。

C#
static void FindUser(int userId)
{
if (userId == 0)
{
throw new UserNotFoundException("ユーザーが見つかりません。");
}
}

7-4. 独自例外に情報を持たせる方法

独自例外には、追加の情報を持たせることもできます。

C#
public class StockShortageException : Exception
{
public string ProductCode { get; }
public int RequestedQuantity { get; }

public StockShortageException(string productCode, int requestedQuantity)
: base($"商品 {productCode} の在庫が不足しています。要求数量: {requestedQuantity}")
{
ProductCode = productCode;
RequestedQuantity = requestedQuantity;
}
}

この例では、商品コードと要求数量を例外クラスに持たせています。

C#
try
{
throw new StockShortageException("P001", 10);
}
catch (StockShortageException ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine(ex.ProductCode);
Console.WriteLine(ex.RequestedQuantity);
}

例外に必要な情報を持たせることで、ログ出力や画面表示、リカバリー処理がしやすくなります。

7-5. 独自例外を使う際の注意点

独自例外は便利ですが、作りすぎると管理が大変になります。

似たような例外クラスが大量に増えると、どれを使えばよいか分かりにくくなります。また、標準例外で十分に表現できるケースでは、無理に独自例外を作る必要はありません。

独自例外を使う判断基準は、catch側でその例外を特別に扱う必要があるかどうかです。

特別な処理が必要なら独自例外を作る価値があります。単にメッセージを表示するだけなら、標準例外で十分な場合もあります。

8. 例外処理の実践サンプル

8-1. 数値変換で発生する例外処理

ユーザー入力を数値に変換する例です。

C#
Console.WriteLine("数量を入力してください。");

try
{
string input = Console.ReadLine();
int quantity = int.Parse(input);

Console.WriteLine($"数量は{quantity}です。");
}
catch (FormatException)
{
Console.WriteLine("数量は数値で入力してください。");
}
catch (OverflowException)
{
Console.WriteLine("入力された数値が大きすぎます。");
}

int.Parseでは、数値形式でない場合にFormatException、数値の範囲を超える場合にOverflowExceptionが発生することがあります。

ただし、入力チェックではTryParseもよく使います。

C#
string input = Console.ReadLine();

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

8-2. ファイル読み込み時の例外処理

ファイル読み込みでは、ファイルが存在しない、アクセス権限がない、他のプロセスが使用中などの理由で例外が発生することがあります。

C#
try
{
string path = "sample.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);
}

ファイル処理では、想定できる例外を分けてcatchすると、ユーザーに分かりやすいメッセージを表示できます。

8-3. null参照を防ぐ例外処理

null参照は、例外処理でcatchするよりも、事前に防ぐことが重要です。

C#
static void PrintLength(string text)
{
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}

Console.WriteLine(text.Length);
}

呼び出し側では、必要に応じて例外をcatchします。

C#
try
{
PrintLength(null);
}
catch (ArgumentNullException ex)
{
Console.WriteLine($"引数がnullです: {ex.ParamName}");
}

このように、メソッドの入り口で不正な値をチェックすると、原因が分かりやすくなります。

8-4. メソッド内で例外を投げる例

業務ルールに違反した場合に例外を投げる例です。

C#
static void Withdraw(int balance, int amount)
{
if (amount <= 0)
{
throw new ArgumentException("出金額は1円以上で指定してください。", nameof(amount));
}

if (balance < amount)
{
throw new InvalidOperationException("残高が不足しています。");
}

Console.WriteLine($"{amount}円を出金しました。");
}

呼び出し側は次のように処理できます。

C#
try
{
Withdraw(1000, 2000);
}
catch (ArgumentException ex)
{
Console.WriteLine($"入力エラー: {ex.Message}");
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"操作エラー: {ex.Message}");
}

引数の問題と状態の問題で例外クラスを分けると、catch側の処理も整理しやすくなります。

8-5. 複数のcatchとfinallyを組み合わせる例

最後に、複数のcatchfinallyを組み合わせた例です。

C#
try
{
Console.WriteLine("ファイル名を入力してください。");
string fileName = Console.ReadLine();

string content = File.ReadAllText(fileName);
int number = int.Parse(content);

Console.WriteLine($"ファイル内の数値は{number}です。");
}
catch (FileNotFoundException)
{
Console.WriteLine("指定されたファイルが見つかりません。");
}
catch (FormatException)
{
Console.WriteLine("ファイルの内容が数値ではありません。");
}
catch (IOException ex)
{
Console.WriteLine("ファイル操作中にエラーが発生しました。");
Console.WriteLine(ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("予期しないエラーが発生しました。");
Console.WriteLine(ex.Message);
}
finally
{
Console.WriteLine("処理を終了します。");
}

このサンプルでは、ファイル読み込み、数値変換、予期しないエラーに分けて処理しています。最後にfinallyで終了メッセージを表示します。

9. 例外処理でやってはいけない書き方

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

空のcatchは避けるべき代表的な書き方です。

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

このコードでは、例外が発生しても何も起きません。プログラムは一見正常に動いているように見えますが、実際には問題が隠れてしまいます。

最低でもログを残す、メッセージを表示する、または必要に応じて再スローしましょう。

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

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

すべての例外をExceptionでcatchする書き方は、場合によっては問題になります。

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

この書き方では、どのような例外が発生したのか分かりにくくなります。また、本来は上位層で処理すべき例外まで処理してしまう可能性があります。

具体的な例外が想定できる場合は、できるだけ個別にcatchしましょう。

C#
catch (FormatException)
{
Console.WriteLine("形式が正しくありません。");
}
catch (IOException)
{
Console.WriteLine("ファイル操作に失敗しました。");
}

9-3. 通常の分岐処理を例外で代用する

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

悪い例は次のとおりです。

C#
try
{
int result = 100 / number;
}
catch (DivideByZeroException)
{
Console.WriteLine("0では割れません。");
}

もちろん例外処理としては動作しますが、numberが0かどうかは事前に判定できます。

C#
if (number == 0)
{
Console.WriteLine("0では割れません。");
}
else
{
int result = 100 / number;
}

予測できる条件はifで処理し、予測しづらい異常な状況に例外処理を使うのが基本です。

9-4. throw exで再スローする

再スローでthrow ex;を使うのは避けましょう。

C#
catch (Exception ex)
{
throw ex;
}

この書き方は、元のスタックトレースを壊してしまう可能性があります。

正しくは次のように書きます。

C#
catch (Exception)
{
throw;
}

ログを残してから再スローする場合も同じです。

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

C# 例外処理では、再スローはthrow;と覚えておきましょう。

9-5. ユーザーにそのまま例外メッセージを表示する

例外メッセージをそのままユーザーに表示するのは避けるべき場合があります。

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

開発中は原因確認に便利ですが、実際のアプリケーションでは、内部情報やファイルパス、SQL、システム構成などが表示されてしまう可能性があります。

ユーザーには分かりやすい一般的なメッセージを表示し、詳細はログに残すのが安全です。

C#
catch (Exception ex)
{
Console.WriteLine("処理中にエラーが発生しました。時間をおいて再度お試しください。");

// 実際の開発ではログ出力ライブラリなどを使う
Console.WriteLine(ex.ToString());
}

9-6. finallyに複雑な処理を書きすぎる

finallyには、後処理を簡潔に書くのが基本です。

C#
finally
{
// 複雑な業務処理を大量に書くのは避ける
}

finallyに複雑な処理を書くと、例外発生時の流れが分かりにくくなります。また、finally内でさらに例外が発生すると、元の例外が見えにくくなることもあります。

finallyには、リソース解放や状態の復元など、確実に実行したい最小限の処理を書くようにしましょう。

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

10-1. 想定できる例外だけをcatchする

C# 例外処理では、想定できる例外だけをcatchするのが基本です。

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

何でもcatchすると、問題の原因が分かりにくくなります。処理できない例外は、無理にcatchせず呼び出し元に任せる判断も重要です。

10-2. 例外メッセージは原因が分かる内容にする

例外メッセージは、原因が分かるように書きましょう。

C#
throw new ArgumentException("商品コードは必須です。", nameof(productCode));

「エラーが発生しました」だけでは、開発者も利用者も原因を判断しにくくなります。

ただし、機密情報を含めないことも大切です。パスワード、個人情報、認証情報などは例外メッセージに含めないようにしましょう。

10-3. ログを残して原因調査しやすくする

例外が発生した場合は、ログを残すことで原因調査がしやすくなります。

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

ex.Messageだけでは情報が不足することがあります。調査用には、スタックトレースを含むex.ToString()が役立ちます。

実務では、Consoleへの出力ではなく、ログ出力ライブラリやアプリケーションのログ基盤を使うことが一般的です。

10-4. 入力チェックで防げる例外は事前に防ぐ

入力チェックで防げる例外は、事前に防ぐのが基本です。

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

数値変換でも、ユーザー入力にはTryParseが向いています。

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

例外処理は便利ですが、通常の入力ミスまで例外で処理すると、コードが読みづらくなることがあります。

10-5. 呼び出し元で処理すべき例外は無理にcatchしない

メソッド内で例外をcatchすべきかどうかは、そのメソッドが適切に対処できるかで判断します。

悪い例は次のようなコードです。

C#
static void LoadFile(string path)
{
try
{
string content = File.ReadAllText(path);
}
catch (IOException)
{
Console.WriteLine("読み込みに失敗しました。");
}
}

このメソッド内でエラー後の対応を決められないなら、無理にcatchしない方がよい場合もあります。

C#
static string LoadFile(string path)
{
return File.ReadAllText(path);
}

呼び出し元で、画面に表示するのか、再試行するのか、処理を中断するのかを判断できます。

10-6. 業務ロジックに合わせて例外設計する

実務では、単にtry-catchを書くだけでなく、業務ロジックに合わせて例外設計を行うことが重要です。

たとえば、注文処理では次のような例外が考えられます。

在庫不足。

支払い未完了。

注文キャンセル済み。

配送先不正。

これらをすべてExceptionで扱うと、原因に応じた処理がしづらくなります。

C#
throw new InvalidOperationException("キャンセル済みの注文は発送できません。");

または、必要に応じて独自例外を作成します。

C#
throw new OrderAlreadyCanceledException("キャンセル済みの注文です。");

例外設計は、アプリケーションの保守性に大きく関わります。

11. 非同期処理における例外処理

11-1. async/awaitでの例外処理の基本

C#の非同期処理でも、基本的にはtry-catchで例外を処理できます。

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

awaitしている非同期メソッド内で例外が発生した場合、その例外はawaitした場所でcatchできます。

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

11-2. Task内で発生した例外をcatchする方法

Task内で発生した例外も、awaitすれば通常の例外と同じようにcatchできます。

C#
try
{
await Task.Run(() =>
{
throw new Exception("Task内で例外が発生しました。");
});
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}

一方、awaitしないまま放置すると、例外の扱いが分かりにくくなります。

C#
Task.Run(() =>
{
throw new Exception("例外");
});

非同期処理では、基本的にTaskを返し、呼び出し元でawaitして例外を処理できるようにすることが大切です。

11-3. 非同期処理でfinallyを使う例

非同期処理でもfinallyは使用できます。

C#
try
{
Console.WriteLine("処理開始");

await Task.Delay(1000);
throw new Exception("エラー");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
finally
{
Console.WriteLine("後処理を実行します。");
}

この例では、非同期処理中に例外が発生しても、最後にfinallyが実行されます。

ローディング表示を戻すような処理にも使えます。

C#
bool isLoading = true;

try
{
await LoadDataAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
finally
{
isLoading = false;
}

11-4. 非同期処理で注意すべき例外の扱い

非同期処理では、async voidに注意が必要です。

C#
async void Execute()
{
await Task.Delay(1000);
throw new Exception("エラー");
}

async voidは呼び出し元でawaitできないため、例外処理が難しくなります。イベントハンドラーなど一部の用途を除き、非同期メソッドはTaskを返すのが基本です。

C#
async Task ExecuteAsync()
{
await Task.Delay(1000);
throw new Exception("エラー");
}

呼び出し側では次のようにcatchできます。

C#
try
{
await ExecuteAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}

非同期処理でも、例外をどこでcatchするかを明確にしておくことが重要です。

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

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

try-catchは、例外が発生したときに適切な対応ができる場所に書くべきです。

たとえば、ユーザーにエラーメッセージを表示する必要があるなら画面側でcatchすることがあります。ログを残して処理を中断したいなら、アプリケーションの上位層でcatchすることもあります。

一方、メソッド内で対処できない例外を無理にcatchする必要はありません。catchしても何もできない場合は、呼び出し元に任せた方がよいこともあります。

12-2. finallyは必ず必要?

finallyは必ず必要ではありません。

後処理が必要な場合に使います。特に、成功しても失敗しても必ず実行したい処理がある場合に便利です。

ただし、ファイルやストリームなどのリソース解放では、using文やusing宣言で対応できることも多いです。

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

後処理がないなら、finallyを書かなくても問題ありません。

12-3. catchしない例外はどうなる?

catchされなかった例外は、呼び出し元へ伝わっていきます。

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

static void B()
{
throw new Exception("エラー");
}

Bで発生した例外がB内でcatchされない場合、呼び出し元のAに伝わります。Aでもcatchされなければ、さらに上位へ伝わります。

最終的にどこでもcatchされなければ、プログラムが異常終了する可能性があります。

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

事前に予測できる条件はif文で処理し、予測しにくい異常な状況は例外処理で扱うのが基本です。

たとえば、入力欄が空かどうかはif文でチェックできます。

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

数値変換も、ユーザー入力ならTryParseで判定できます。

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

一方、ファイル読み込み中の予期しない入出力エラーなどは、例外処理で扱うのが適しています。

12-5. Exceptionをそのまま使ってもよい?

学習用のサンプルではExceptionを使っても構いません。

C#
throw new Exception("エラーが発生しました。");

ただし、実務ではできるだけ具体的な例外クラスを使う方がよいです。

引数が不正ならArgumentException

null引数ならArgumentNullException

状態が不正ならInvalidOperationException

形式が不正ならFormatException

具体的な例外を使うことで、エラーの意味が明確になり、catch側でも扱いやすくなります。

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

初心者がまず覚えるべきなのは、try-catchの基本です。

C#
try
{
// 例外が発生する可能性のある処理
}
catch (Exception ex)
{
// 例外が発生したときの処理
Console.WriteLine(ex.Message);
}

次に、例外の種類ごとにcatchを分ける方法を覚えましょう。

C#
catch (FormatException)
{
Console.WriteLine("形式が正しくありません。");
}
catch (IOException)
{
Console.WriteLine("ファイル操作に失敗しました。");
}

さらに、後処理のfinally、例外を投げるthrow、再スローのthrow;まで理解できれば、C# 例外処理の基本は十分に身につきます。

まとめ

C#の例外処理は、プログラムの実行中に発生する異常な状態に対応するための重要な仕組みです。

基本は、例外が発生する可能性のある処理をtryに書き、発生した例外をcatchで受け取ることです。

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

最後に必ず実行したい後処理がある場合は、finallyを使います。

C#
finally
{
Console.WriteLine("後処理を実行します。");
}

不正な引数や業務ルール違反を呼び出し元に知らせたい場合は、throwで例外を発生させます。

C#
throw new ArgumentException("引数が不正です。");

また、catchした例外を再スローする場合は、スタックトレースを壊さないようにthrow;を使いましょう。

C#
catch (Exception)
{
throw;
}

C# 例外処理で大切なのは、何でもcatchすることではありません。想定できる例外を適切に扱い、防げる例外は事前チェックで防ぎ、対処できない例外は呼び出し元に任せることです。

try-catchfinallythrowの役割を理解し、例外クラスを適切に選べるようになると、保守性が高く、安全に動作するC#プログラムを書けるようになります。