前回は、nullとnull許容型の基本について解説しました。
今回は、C#の例外処理(try-catch)の基本と実務でのポイントについて、網羅的かつわかりやすく解説します。
例外処理(try-catch)とは?
C#の例外(Exception)とは、プログラムの実行中に発生するエラーのことで、例外が発生するとプログラムは停止してしまいます。
例外処理とは、予期せぬ例外を安全に処理して、アプリやシステムが止まらないようにする仕組みです。
C#には「try-catch」という例外処理の構文が用意されています。
- 「try」はエラーが発生する可能性があるコードを記述する場所
- 「catch」はtryの中でエラーが起きたら行う処理を記述する場所
実際のコード例を見ながらtry-catchの基本的な使い方を確認してみましょう。
try-catchのサンプルコード
以下はテキストファイルを開いて中身を読み込むコードの例です。
StreamReader sr = null;
try
{
// 例外が起きる可能性がある処理(「input.txt」というファイルを読み込む)
sr = new StreamReader("input.txt");
Console.WriteLine(sr.ReadToEnd());
}
catch (FileNotFoundException ex)
{
// 例外(ファイルが見つからないエラー)が発生した時の処理
Console.WriteLine("ファイルが見つかりませんでした:" + ex.Message);
}
もし「input.txt」が存在しない状態でこのコードを実行すると、FileNotFoundExceptionという例外が発生しますが、try-catchのおかげでプログラムは停止せずに、catchブロックの中で設定した安全な処理が実行されます。
具体的には、以下のようなメッセージがユーザーに表示されます。
ファイルが見つかりませんでした:Could not find file 'C:\アプリがあるフォルダ\input.txt'.
このように、「try-catch」の仕組みを使うことで、例外が発生しても安全にプログラムを動かし続けることができます。
finallyブロックを使った後処理
実際の開発では「例外が起きても起きなくても、必ず実行したい処理」が必要になることがあります。
例えば、開いたファイルやデータベース接続を閉じてリソースを解放する場合などです。
このようなときに役立つのが、finallyブロックです。
先ほどのサンプルコードにfinallyブロックを追加したのが以下です。
StreamReader sr = null;
try
{
sr = new StreamReader("input.txt");
Console.WriteLine(sr.ReadToEnd());
}
catch (FileNotFoundException ex)
{
Console.WriteLine("ファイルが見つかりませんでした:" + ex.Message);
}
finally
{
// エラーが起きても起きなくても最後にファイルを閉じる
if (sr != null)
{
sr.Close();
}
}
この例では、もしファイルの読み込みに失敗しても、finallyブロック内のsr.Close()
が必ず実行されるので、リソースの無駄遣いや不具合を防ぐことができます。
using文でさらにシンプルに書く
C#には、リソース管理をより簡単にしてくれる「using文」という仕組みもあります。
using文を使うと、ブロックの終わりで自動的にリソースが解放されるので、finallyブロックを書かなくても済みます。
try
{
using (var sr = new StreamReader("input.txt"))
{
Console.WriteLine(sr.ReadToEnd());
} // ブロックを抜けるとsrが自動的に閉じられる
}
catch (Exception ex)
{
Console.WriteLine("エラー:" + ex.Message);
}
using文は、ファイルだけでなくデータベース接続やネットワークリソースの管理にもよく使われます。
リソースを扱う際は、finallyブロックやusing文を活用して、後片付けを忘れずに行いましょう。
実務でよく使う例外クラス
C#には、エラーの種類ごとに様々な「例外クラス」が標準で用意されています。
実際の開発でよく登場する代表的な例外クラスを紹介します。
- ArgumentException
メソッドの引数が不正なときに発生。 - ArgumentNullException
メソッドの引数がnullで許可されないときに発生。 - ArgumentOutOfRangeException
メソッドの引数が許容範囲外のときに発生。 - InvalidOperationException
オブジェクトの状態で操作が不適切なときに発生。 - NullReferenceException
nullのオブジェクトにアクセスしたときに発生。 - DivideByZeroException
0で割ろうとしたときに発生。 - OverflowException
算術演算が型の範囲を超えたときに発生。 - IndexOutOfRangeException
配列やリストで範囲外のインデックスにアクセスしたときに発生。 - KeyNotFoundException
Dictionaryなどで指定したキーが見つからないときに発生。 - IOException
ファイルやストリームの入出力エラー。 - FileNotFoundException
ファイルが見つからないときに発生。 - NotImplementedException
実装されていないメソッドなどが呼ばれたときに発生。
特に「NullReferenceException」は開発で最もよく遭遇する例外です。事前にnullかどうかを確認する習慣をつけるようにしましょう。
※nullの扱いについては前回の記事「nullとnull許容型の基本」を参照してください。
C#では、標準の例外クラスだけでなく、自分で「カスタム例外」クラスを作ることもできます。初心者のうちは使う場面は少ないので詳しくは触れませんが、業務で特別なルールのエラーを表現したい場合などに使われます。
例外を自分で投げる(throw)
ここまでは、プログラムが勝手に発生させる例外の処理方法について説明してきましたが、開発者自身が「ここはエラーにしたい」と判断したときに、意図的に例外を発生させることもできます。
例えば、メソッドの引数に「あり得ない値」が渡ってきた場合などに使います。
public void SetAge(int age)
{
if (age < 0)
{
// 年齢がマイナスはあり得ないので意図的に例外を発生させる
throw new ArgumentOutOfRangeException(nameof(age), "年齢に負の値は指定できません。");
}
// 正しい場合の処理...
}
このようにthrow
を使って例外を発生させることで、「ここで問題が起きている」というサインを他の開発者やプログラム全体に伝えることができます。
また、.NET 8以降のC#では、よくある引数チェックのために「ThrowIf〇〇」という便利なメソッドが用意されています。
これを使うと、先ほどのSetAge
メソッドもさらにシンプルに書けます。
public void SetAge(int age)
{
// ageが負の値ならArgumentOutOfRangeExceptionを発生させる
ArgumentOutOfRangeException.ThrowIfNegative(age, nameof(age));
// 正しい場合の処理...
}
他にも便利なメソッドが標準で用意されているので、積極的に活用しましょう。
nullチェック(.NET 7以降)
// objがnullの場合はArgumentNullExceptionを発生させる
ArgumentNullException.ThrowIfNull(obj);
文字列がnullまたは空文字かどうかのチェック(.NET 7以降)
// strがnullまたは空文字の場合はArgumentExceptionを発生させる
ArgumentException.ThrowIfNullOrEmpty(str);
0や負の値のチェック(.NET 8以降)
// numが0の場合はArgumentOutOfRangeExceptionを発生させる
ArgumentOutOfRangeException.ThrowIfZero(num);
// numがマイナスの場合はArgumentOutOfRangeExceptionを発生させる
ArgumentOutOfRangeException.ThrowIfNegative(num);
// numが0またはマイナスの場合はArgumentOutOfRangeExceptionを発生させる
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(num);
ArgumentOutOfRangeExceptionの他の便利メソッドについては下記ドキュメントを参照してください。
https://learn.microsoft.com/ja-jp/dotnet/api/system.argumentoutofrangeexception?view=net-8.0#methods
アプリケーション全体での例外処理(グローバル例外処理)
ここまでで、例外が起きそうな場所にtry-catchを書いて処理する方法を説明しましたが、実際のアプリケーションでは「どこにもcatchを書いていなかったエラー」や「予想外のエラー」がどうしても発生します。
このような「キャッチしきれなかった例外」をまとめて安全に処理するために、アプリ全体で例外を管理する仕組み(グローバル例外処理)を用意するのが一般的です。
例外処理はtry-catchだけでなくグローバル例外処理も含めて考えないと理解しづらいので、ぜひここで考え方を押さえておきましょう。
グローバル例外処理の考え方
グローバル例外処理は、プログラムの一番上の層でプログラム内で起きた例外を最後に受け止める役割を持っています。
これによって、エラーの詳細な情報を開発者向けのログとして残して、ユーザーには分かりやすいメッセージだけを表示するといったことが実現できます。
この考え方はWebアプリやデスクトップアプリ、バッチ処理など、どんな種類のアプリケーションでも共通して使えます。
Webアプリ(ASP.NET Core)での例
ASP.NET CoreというWebアプリケーションフレームワークでは、Program.cs
内の「ミドルウェア」と呼ばれる部分でグローバル例外処理を設定できます。
以下はその一例です。
app.UseExceptionHandler(builder =>
{
builder.Run(async context =>
{
// アプリ内で発生した未処理の(catchしていない)例外を取得
var exceptionFeature = context.Features.Get<IExceptionHandlerPathFeature>();
var exception = exceptionFeature?.Error;
// 開発者向けに詳細なエラー情報をログに残す
Console.WriteLine($"未処理の例外が発生しました:{exception?.ToString()}");
// ユーザーには分かりやすいエラーメッセージのみを表示
context.Response.StatusCode = 500;
await context.Response.WriteAsync("サーバーエラーが発生しました。再度お試しください。");
});
});
このようにしておくと、プログラム内で起きた例外を逃さずに安全に処理できます。
詳細な例外の内容は開発者向けにログに残し、ユーザーには余計な情報を見せずに「再度お試しください」といったメッセージのみを返すというのはよくある設計です。
例外処理のベストプラクティス
最後に、実務の現場における例外処理の基本的なルールを、具体的なサンプルコードを交えて解説します。
例外は具体的にキャッチする
catch
を使う時には、可能な限り具体的な例外クラスを指定しましょう。
全ての例外をまとめてキャッチするのではなく、起こりうる例外ごとに分けて処理すると、エラーの原因が特定しやすくなります。
以下はファイル操作の例です。
try
{
string text = File.ReadAllText("example.txt");
Console.WriteLine(text);
}
catch (FileNotFoundException ex)
{
// ファイルが無い場合だけの対応
Console.WriteLine("ファイルが見つかりませんでした:" + ex.Message);
}
catch (IOException ex)
{
// それ以外のファイル操作のエラー
Console.WriteLine("ファイルの読み込み中にエラーが発生しました:" + ex.Message);
}
catch (Exception ex)
{
// 上記以外の予期しないエラー
Console.WriteLine("予期せぬエラーが発生しました:" + ex.Message);
}
このように、想定可能な例外は個別にcatchして、最後に全体をcatch(Exception)でまとめておくのが基本です。
具体的な例外ごとにメッセージや処理を変えられるので、エラーの原因が特定しやすくなり、バグの発見や修正もしやすくなります。
例外を握り潰さない
初心者がやりがちな失敗として、エラーをキャッチしたにもかかわらず「何もしない」というケースがあります。
エラーが起きたことを記録したり、ユーザーに伝えたりしないまま無視してしまうことを「例外を握り潰す」と言います。
例外を握り潰してしまうと、どこで何が起きているのかが分からなくなってしまい、バグの発見や修正が困難になります。
悪い例(握り潰し)
try
{
var result = CalculateData();
}
catch (Exception ex)
{
// 何もしない(これはNG)
}
良い例(適切な処理)
try
{
var result = CalculateData();
}
catch (Exception ex)
{
// エラーの内容を記録し、必要なら上位に伝える
Console.WriteLine("エラーが発生しました:" + ex.Message);
throw; // 例外を上位に伝える(再スロー)
}
このように、最低限ログを出力するだけでも、後から原因を調べるのに大きく役立ちます。
場合によっては「このエラーは自分のところでは対処できない」と判断して、上位の処理(グローバル例外処理など)に例外を再スロー(throw;
)するのも良い対応です。
catchブロックの中にthrow;
だけを書くのは、何の処理もせずに例外を上位にそのまま伝えるだけなので、基本的には意味がありません。
再スローする場合は、せめてエラーの内容を記録したり、何かしらの追加処理を行ってからthrow;を使うようにしましょう。
詳細なログを残す
例外が発生したら、エラーの内容だけでなく、コードの何行目で起きたのか(スタックトレース)まで記録することが大切です。
具体的には以下のようなログ記録が望ましいです。
try
{
SaveToDatabase(data);
}
catch (Exception ex)
{
// 詳細なエラー情報をログに記録する
LogError(ex.ToString()); // ex.ToString()でスタックトレースも記録される
throw;
}
ログには詳細情報が含まれるため、後からエラーを調査する際に役立ちます。
頻繁に発生しそうなエラーは事前チェックする
例外処理(try-catch)は便利ですが、発生した例外をキャッチする処理は他の処理に比べて重いので、例外が頻発するとシステムのパフォーマンスが低下します。
そのため、よくある例外や事前に予測できる例外は、なるべくif文などで事前に処理することが大事です。
良くない例(例外に頼る場合)
try
{
var numbers = new List<int> { 1, 2, 3 };
Console.WriteLine(numbers[5]); // リストに存在しないインデックスを指定すると例外が発生する
}
catch (ArgumentOutOfRangeException ex)
{
Console.WriteLine("範囲外の要素です:" + ex.Message);
}
良い例(事前にチェックする場合)
var numbers = new List<int> { 1, 2, 3 };
var index = 5;
if (0 <= index && index < numbers.Count)
{
Console.WriteLine(numbers[index]);
}
else
{
Console.WriteLine("指定されたインデックスは範囲外です。");
}
このように事前のチェックを行い、例外に頼りすぎないことを心がけましょう。
まとめ
今回は、C#における例外処理の基本や、実際の開発で役立つポイントを解説しました。
try-catchの使い方やエラーごとの適切な対処方法を知っておくことで、プログラムの安全性や信頼性が大きく高まります。
初心者のうちから例外処理を理解しておくと、後々の開発でも必ず役立ちます。ぜひ今回紹介した内容を、実際に書いて試してみてください。
次回は、C#開発で非常に重要な「非同期処理(async/await)」について解説します。
参考ドキュメント


