C#のエラー処理完全ガイド|try-catchの基本から例外設計・ログ出力まで解説
はじめに
C#でアプリケーションを開発していると、ファイルが見つからない、入力値の形式が正しくない、通信先のAPIが応答しない、データベース接続に失敗するなど、さまざまなエラーに直面します。こうした問題を想定せずに実装してしまうと、アプリケーションが突然停止したり、ユーザーに不親切なエラーメッセージが表示されたり、原因調査に時間がかかったりします。
C#のエラー処理では、主に例外処理を使います。代表的な構文がtry-catch-finallyです。ただし、単にtry-catchで囲めばよいわけではありません。どの例外を捕捉するのか、どこでログを出すのか、ユーザーには何を伝えるのか、復旧できるエラーなのかを考えて設計する必要があります。
この記事では、C#のエラー処理について、try-catchの基本から、例外クラスの選び方、throwの使い方、非同期処理での例外処理、ログ出力、実務でのベストプラクティスまで体系的に解説します。
1. C#のエラー処理とは
C#のエラー処理とは、プログラム実行中に発生する想定外または異常な状態に対して、アプリケーションを安全に継続・終了・復旧させるための仕組みです。
たとえば、次のような状況はエラー処理の対象になります。
C#string text = File.ReadAllText("config.json");
このコードは一見シンプルですが、config.jsonが存在しない場合、アクセス権限がない場合、ファイルが別プロセスに使用されている場合などに例外が発生します。こうしたケースを考慮していないと、アプリケーションは異常終了してしまいます。
C#では、例外を使って異常な状態を通知し、try-catchでそれを捕捉して適切に処理します。
1-1. エラー処理が必要になる理由
エラー処理が必要な理由は、プログラムが常に理想的な状態で動くとは限らないからです。
開発中は正常なデータや環境で動作確認をすることが多いですが、本番環境ではさまざまな問題が発生します。ユーザーが想定外の値を入力することもあれば、ネットワークが一時的に切断されることもあります。外部APIの仕様変更、データベースのタイムアウト、ファイルの破損など、アプリケーションの外側で起きる問題も少なくありません。
エラー処理を適切に実装することで、次のような効果が得られます。
アプリケーションの突然の停止を防げます。ユーザーに分かりやすいメッセージを表示できます。ログを残すことで原因調査がしやすくなります。復旧可能なエラーであれば再試行や代替処理ができます。セキュリティ上危険な内部情報の露出を防げます。
つまり、C#のエラー処理は単なる構文の知識ではなく、安定したアプリケーションを作るための重要な設計要素です。
1-2. C#における「エラー」と「例外」の違い
一般的に「エラー」という言葉は、プログラムで発生する問題全般を指します。一方、C#における「例外」は、実行時に発生した異常を表現するオブジェクトです。
たとえば、ファイルが存在しないこと自体はエラーですが、C#ではそれをFileNotFoundExceptionという例外として扱います。文字列を数値に変換できない場合はFormatExceptionが発生します。オブジェクトがnullなのにメンバーへアクセスした場合はNullReferenceExceptionが発生します。
つまり、エラーは概念であり、例外はC#でそのエラーを扱うための仕組みと考えると分かりやすいです。
C#try
{
int number = int.Parse("abc");
}
catch (FormatException ex)
{
Console.WriteLine("数値に変換できませんでした。");
}
この例では、「abcを数値に変換できない」というエラーが発生し、それがFormatExceptionという例外として通知されています。
1-3. 例外処理を正しく設計しない場合のリスク
例外処理を正しく設計しないと、さまざまな問題が発生します。
まず、例外をまったく捕捉しない場合、アプリケーションが突然終了する可能性があります。デスクトップアプリであれば画面が落ち、Webアプリであれば500エラーが返り、バッチ処理であれば途中で停止します。
逆に、すべての例外を雑に捕捉してしまうのも危険です。
C#try
{
Execute();
}
catch
{
}
このような空のcatchを書くと、エラーが発生しても何も分かりません。ログも残らず、ユーザーにも通知されず、データ不整合だけが残る可能性があります。
また、例外メッセージをそのままユーザーに表示すると、ファイルパス、SQL文、内部クラス名などの機密情報が漏れることがあります。エラー処理は、安定性だけでなくセキュリティにも関わる重要な実装です。
1-4. この記事で学べること
この記事では、C#のエラー処理について次の内容を学べます。
try-catch-finallyの基本構文、代表的な例外クラス、実務でよくあるエラー処理パターン、throwの正しい使い方、例外設計の考え方、ログ出力のポイント、非同期処理における例外処理、避けるべきアンチパターン、C#のエラー処理ベストプラクティスを順番に解説します。
初心者の方は基本構文から理解でき、実務経験者の方は例外設計やログ設計の見直しに役立つ内容になっています。
2. C#の例外処理の基本|try-catch-finally
C#の例外処理で中心となるのがtry-catch-finallyです。
tryブロックには例外が発生する可能性のある処理を書きます。catchブロックには例外が発生したときの処理を書きます。finallyブロックには、例外の有無にかかわらず必ず実行したい後処理を書きます。
2-1. try-catchの基本構文
基本的なtry-catchの構文は次のとおりです。
C#try
{
// 例外が発生する可能性のある処理
}
catch (Exception ex)
{
// 例外が発生したときの処理
}
たとえば、数値変換で例外を捕捉する場合は次のように書きます。
C#try
{
int value = int.Parse("abc");
Console.WriteLine(value);
}
catch (FormatException ex)
{
Console.WriteLine("数値の形式が正しくありません。");
}
int.Parse("abc")は数値に変換できないため、FormatExceptionが発生します。その例外をcatchで捕捉し、エラーメッセージを表示しています。
重要なのは、tryの中に何でも入れすぎないことです。例外が発生する可能性のある処理を必要な範囲だけ囲むことで、どこで問題が起きたのかを把握しやすくなります。
2-2. catchで例外を捕捉する方法
catchでは、捕捉したい例外型を指定できます。
C#try
{
string text = File.ReadAllText("sample.txt");
}
catch (FileNotFoundException ex)
{
Console.WriteLine("ファイルが見つかりません。");
}
この例では、ファイルが存在しない場合に発生するFileNotFoundExceptionを捕捉しています。
より広い範囲の例外を捕捉したい場合は、基底クラスであるExceptionを指定できます。
C#try
{
string text = File.ReadAllText("sample.txt");
}
catch (Exception ex)
{
Console.WriteLine("エラーが発生しました。");
}
ただし、常にExceptionで捕捉するのはおすすめできません。どのようなエラーが起きるか分かっている場合は、できるだけ具体的な例外型を指定しましょう。
2-3. finallyの役割と使いどころ
finallyは、例外が発生してもしなくても必ず実行されるブロックです。
C#FileStream? stream = null;
try
{
stream = new FileStream("sample.txt", FileMode.Open);
// ファイル処理
}
catch (IOException ex)
{
Console.WriteLine("ファイル処理中にエラーが発生しました。");
}
finally
{
stream?.Dispose();
}
この例では、ファイルを開いた後、最後にDisposeを呼び出してリソースを解放しています。例外が発生してもfinallyは実行されるため、ファイルやネットワーク接続などの後始末に使えます。
ただし、現在のC#ではusing文やusing宣言を使うことで、リソース解放をより簡潔に書けます。
C#using var stream = new FileStream("sample.txt", FileMode.Open);
そのため、単純なリソース解放であればfinallyよりもusingを使う場面が多いです。finallyは、ロック解除、一時ファイル削除、状態の復元など、例外の有無にかかわらず実行したい処理に適しています。
2-4. 複数のcatchを使い分ける方法
C#では、複数のcatchを並べて例外の種類ごとに処理を分けられます。
C#try
{
string text = File.ReadAllText("sample.txt");
int number = int.Parse(text);
}
catch (FileNotFoundException ex)
{
Console.WriteLine("ファイルが見つかりません。");
}
catch (FormatException ex)
{
Console.WriteLine("ファイルの内容が数値ではありません。");
}
catch (IOException ex)
{
Console.WriteLine("ファイルの読み込み中にエラーが発生しました。");
}
複数のcatchを書くときは、具体的な例外型から先に書く必要があります。Exceptionのような広い型を先に書くと、それ以降の具体的なcatchに到達できなくなります。
C#try
{
// 処理
}
catch (FormatException ex)
{
// 具体的な例外
}
catch (Exception ex)
{
// 最後に広い例外
}
また、例外フィルターを使うと、条件に応じて捕捉するかどうかを制御できます。
C#try
{
// 処理
}
catch (IOException ex) when (ex.Message.Contains("access"))
{
Console.WriteLine("アクセスに関するエラーです。");
}
2-5. Exceptionクラスの基本プロパティ
C#の多くの例外クラスはSystem.Exceptionを継承しています。Exceptionクラスには、エラー調査に役立つプロパティがあります。
代表的なプロパティは次のとおりです。
Messageは例外の説明文です。StackTraceは例外が発生した場所までの呼び出し履歴です。InnerExceptionは元の原因となった例外を保持します。Sourceは例外を発生させたアプリケーションまたはオブジェクト名を表します。TargetSiteは例外が発生したメソッド情報を表します。
実務では、Messageだけでなく、StackTraceやInnerExceptionを含めてログに残すことが重要です。
C#try
{
Execute();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine(ex.StackTrace);
}
ただし、ユーザーにStackTraceをそのまま表示してはいけません。開発者向けのログとユーザー向けのメッセージは分けて扱いましょう。
3. よく使うC#の例外クラス
C#には多くの例外クラスがあります。すべてを覚える必要はありませんが、よく使う例外を理解しておくと、適切なエラー処理を書きやすくなります。
3-1. NullReferenceException
NullReferenceExceptionは、nullのオブジェクトに対してメンバーへアクセスしたときに発生します。
C#string? name = null;
Console.WriteLine(name.Length);
このコードでは、nameがnullであるにもかかわらずLengthにアクセスしているため、NullReferenceExceptionが発生します。
この例外は、基本的には事前の設計やチェックで防ぐべきです。
C#if (name != null)
{
Console.WriteLine(name.Length);
}
また、C#のnull条件演算子を使うこともできます。
C#Console.WriteLine(name?.Length);
nullable参照型を有効にすると、コンパイル時にnullの可能性を検出しやすくなります。
C#string? name = GetName();
NullReferenceExceptionは、発生してからcatchで処理するというより、発生しないように設計することが重要です。
3-2. ArgumentException/ArgumentNullException
ArgumentExceptionは、メソッドに渡された引数が不正な場合に使います。ArgumentNullExceptionは、引数がnullであってはいけないのにnullが渡された場合に使います。
C#public void RegisterUser(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("ユーザー名は必須です。", nameof(name));
}
// 登録処理
}
nullを明確に禁止したい場合は、ArgumentNullExceptionを使います。
C#public void Save(User user)
{
if (user is null)
{
throw new ArgumentNullException(nameof(user));
}
// 保存処理
}
引数チェックでは、どの引数が問題だったのかを示すためにnameofを使うのがおすすめです。変数名を文字列で直接書くより、リファクタリングに強くなります。
3-3. InvalidOperationException
InvalidOperationExceptionは、オブジェクトの現在の状態では操作を実行できない場合に使います。
C#public class Order
{
public bool IsSubmitted { get; private set; }
public void Cancel()
{
if (IsSubmitted)
{
throw new InvalidOperationException("確定済みの注文はキャンセルできません。");
}
// キャンセル処理
}
}
引数が不正ならArgumentException、オブジェクトの状態が不正ならInvalidOperationExceptionと考えると使い分けやすいです。
たとえば、「キャンセル対象の注文IDが空」はArgumentExceptionですが、「すでに出荷済みなのでキャンセルできない」はInvalidOperationExceptionが適しています。
3-4. FormatException
FormatExceptionは、文字列の形式が期待した形式と異なる場合に発生します。
C#int number = int.Parse("abc");
このコードでは、abcを整数に変換できないためFormatExceptionが発生します。
ユーザー入力を扱う場合、ParseよりもTryParseを使う方が安全です。
C#if (int.TryParse("abc", out int number))
{
Console.WriteLine(number);
}
else
{
Console.WriteLine("数値を入力してください。");
}
FormatExceptionは、日付、数値、JSON、CSVなどの文字列変換でよく登場します。入力値の検証やTryParse系メソッドを活用して、不要な例外を避けることが大切です。
3-5. IOException
IOExceptionは、入出力処理で発生する例外の基底的なクラスです。ファイル読み書き、ストリーム処理、デバイスアクセスなどで発生します。
C#try
{
string text = File.ReadAllText("data.txt");
}
catch (IOException ex)
{
Console.WriteLine("ファイルの読み込みに失敗しました。");
}
ファイルが存在しない場合はFileNotFoundException、ディレクトリが存在しない場合はDirectoryNotFoundException、アクセス権限がない場合はUnauthorizedAccessExceptionが発生することもあります。
ファイル処理では、単に「読み込めなかった」と扱うだけでなく、存在チェック、権限、ファイルロック、文字コード、ディスク容量なども考慮しましょう。
3-6. 自分で例外クラスを選ぶときの考え方
例外クラスを選ぶときは、何が不正なのかを考えることが重要です。
メソッドの引数が不正ならArgumentExceptionまたはその派生クラスを使います。引数がnullならArgumentNullExceptionを使います。現在の状態では操作できないならInvalidOperationExceptionを使います。文字列形式が不正ならFormatExceptionを使います。ファイルやストリームの問題ならIOException系を使います。
独自の業務エラーを表したい場合は、カスタム例外を作ることもあります。ただし、むやみにカスタム例外を増やす必要はありません。既存の例外で意味が伝わるなら、まずは標準の例外クラスを使うのが基本です。
4. try-catchの実践パターン
ここでは、実務でよく使うC#のエラー処理パターンを紹介します。ファイル読み込み、数値変換、API通信、データベース処理、ユーザー入力など、現場で遭遇しやすいケースを見ていきましょう。
4-1. ファイル読み込み時のエラー処理
ファイル読み込みでは、ファイルが存在しない、アクセス権限がない、読み込み中にIOエラーが発生するなどのケースを考慮します。
C#public string ReadConfig(string path)
{
try
{
return File.ReadAllText(path);
}
catch (FileNotFoundException ex)
{
Console.WriteLine("設定ファイルが見つかりません。");
throw;
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("設定ファイルへのアクセス権限がありません。");
throw;
}
catch (IOException ex)
{
Console.WriteLine("設定ファイルの読み込み中にエラーが発生しました。");
throw;
}
}
ここで重要なのは、ログやメッセージを出したあとにthrow;で再スローしている点です。呼び出し元で処理すべき重大なエラーであれば、握りつぶさずに上位へ伝える必要があります。
ユーザー向けアプリケーションであれば、上位層で「設定ファイルを読み込めませんでした。管理者にお問い合わせください。」のような分かりやすいメッセージに変換するとよいでしょう。
4-2. 数値変換時のエラー処理
ユーザー入力やCSV読み込みでは、文字列を数値に変換する処理がよくあります。この場合、例外を使うよりTryParseを使うのが一般的です。
C#string input = "123";
if (int.TryParse(input, out int number))
{
Console.WriteLine($"変換結果: {number}");
}
else
{
Console.WriteLine("数値を入力してください。");
}
int.Parseを使うと、変換できない場合にFormatExceptionが発生します。
C#try
{
int number = int.Parse(input);
}
catch (FormatException)
{
Console.WriteLine("数値の形式が正しくありません。");
}
しかし、ユーザー入力の失敗はよくある通常のケースです。このような場合に毎回例外を発生させると、コードの意図が分かりにくくなります。入力検証ではTryParseのように、成功・失敗を戻り値で表す方法が適しています。
4-3. API通信時のエラー処理
API通信では、ネットワークエラー、タイムアウト、HTTPステータスコードのエラー、レスポンス形式の不正などを考慮する必要があります。
C#public async Task<string> GetUserAsync(HttpClient httpClient, string url)
{
try
{
using var response = await httpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException(
$"API request failed. StatusCode: {response.StatusCode}");
}
return await response.Content.ReadAsStringAsync();
}
catch (HttpRequestException ex)
{
Console.WriteLine("API通信に失敗しました。");
throw;
}
catch (TaskCanceledException ex)
{
Console.WriteLine("API通信がタイムアウトまたはキャンセルされました。");
throw;
}
}
API通信のエラー処理では、再試行できるエラーかどうかを判断することが重要です。たとえば、一時的なネットワーク障害やHTTP 503は再試行の対象になることがあります。一方、HTTP 400のようなリクエスト不正は、再試行しても成功しない可能性が高いです。
また、ログにはURL、HTTPメソッド、ステータスコード、相関IDなどを残すと調査しやすくなります。ただし、アクセストークンや個人情報をログに出してはいけません。
4-4. データベース処理時のエラー処理
データベース処理では、接続エラー、タイムアウト、制約違反、トランザクション失敗などを扱います。
C#public async Task SaveUserAsync(User user)
{
try
{
// データベース保存処理
await repository.SaveAsync(user);
}
catch (DbUpdateException ex)
{
Console.WriteLine("データベース更新に失敗しました。");
throw;
}
catch (TimeoutException ex)
{
Console.WriteLine("データベース処理がタイムアウトしました。");
throw;
}
}
Entity Framework Coreを使っている場合は、更新時にDbUpdateExceptionが発生することがあります。実務では、DB固有のエラーコードを確認して、一意制約違反、外部キー制約違反、接続失敗などを分類することもあります。
トランザクションを使う場合は、失敗時にロールバックする必要があります。
C#await using var transaction = await dbContext.Database.BeginTransactionAsync();
try
{
await dbContext.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
このように、データベース処理ではデータの整合性を守るために、例外処理とトランザクション設計をセットで考えることが重要です。
4-5. ユーザー入力に対するエラー処理
ユーザー入力に対しては、例外を起こしてから処理するのではなく、事前に検証するのが基本です。
C#public string ValidateEmail(string email)
{
if (string.IsNullOrWhiteSpace(email))
{
return "メールアドレスを入力してください。";
}
if (!email.Contains("@"))
{
return "メールアドレスの形式が正しくありません。";
}
return string.Empty;
}
Webアプリケーションでは、モデルバリデーションを使って入力チェックを行うことが多いです。入力不備はシステム異常ではなく、ユーザーが修正できる通常の状態です。そのため、try-catchで処理するより、バリデーションエラーとして扱う方が自然です。
一方で、入力値を使った処理中に予期しないエラーが発生することもあります。その場合は、内部ログには詳細を残し、ユーザーには「処理中に問題が発生しました。時間をおいて再度お試しください。」のような安全なメッセージを表示します。
5. 例外を投げる方法|throwの使い方
C#では、throwを使って明示的に例外を発生させることができます。例外を投げることは、メソッドが正常に処理を続けられない状態であることを呼び出し元へ知らせる手段です。
5-1. throwで例外を発生させる基本
基本的な書き方は次のとおりです。
C#throw new InvalidOperationException("現在の状態では処理を実行できません。");
実際のメソッドでは、引数チェックや状態チェックで使われることが多いです。
C#public void Withdraw(decimal amount)
{
if (amount <= 0)
{
throw new ArgumentException("出金額は0より大きい値を指定してください。", nameof(amount));
}
if (Balance < amount)
{
throw new InvalidOperationException("残高が不足しています。");
}
Balance -= amount;
}
この例では、引数として渡されたamountが不正な場合はArgumentException、口座の状態として残高不足の場合はInvalidOperationExceptionを投げています。
5-2. throw new Exceptionを避けるべき理由
throw new Exception()は、できるだけ避けるべきです。
C#throw new Exception("エラーが発生しました。");
この書き方でも例外は投げられますが、例外の意味が曖昧になります。呼び出し元は、引数が悪いのか、状態が悪いのか、外部リソースに問題があるのかを判断しづらくなります。
代わりに、状況に合った具体的な例外型を使いましょう。
C#throw new ArgumentException("ユーザーIDが不正です。", nameof(userId));
throw new InvalidOperationException("この注文はすでに確定されています。");
throw new FileNotFoundException("設定ファイルが見つかりません。", filePath);
例外型が具体的であれば、呼び出し元で適切にcatchしやすくなり、ログを見たときにも原因を把握しやすくなります。
5-3. throwとthrow exの違い
catchした例外を再度投げる場合は、throw;を使います。
C#try
{
Execute();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
throw;
}
一方、次のようにthrow ex;と書くのは避けるべきです。
C#try
{
Execute();
}
catch (Exception ex)
{
throw ex;
}
throw ex;を使うと、スタックトレースが再スローした位置からの情報に変わってしまい、元の例外発生箇所を追いにくくなります。原因調査のためには、元のスタックトレースを保持するthrow;を使うのが基本です。
例外に情報を追加したい場合は、InnerExceptionを使って新しい例外で包む方法があります。
C#try
{
LoadConfig();
}
catch (IOException ex)
{
throw new InvalidOperationException("設定ファイルの読み込みに失敗しました。", ex);
}
5-4. 例外メッセージの書き方
例外メッセージは、原因調査に役立つよう具体的に書くことが大切です。
悪い例は次のようなメッセージです。
C#throw new InvalidOperationException("エラーが発生しました。");
これでは、何が原因なのか分かりません。
良い例は次のようなメッセージです。
C#throw new InvalidOperationException("確定済みの注文はキャンセルできません。");
さらに、開発者向けには処理対象のIDなどを含めると調査しやすくなります。
C#throw new InvalidOperationException($"注文ID {orderId} は確定済みのためキャンセルできません。");
ただし、パスワード、アクセストークン、クレジットカード番号、個人情報などを例外メッセージに含めてはいけません。例外メッセージはログに出力される可能性があるため、機密情報を含めない設計が必要です。
5-5. InnerExceptionを使った原因の保持
InnerExceptionは、元の例外を保持するための仕組みです。下位層で発生した例外を上位層の意味に変換したい場合に使います。
C#public User LoadUser(int id)
{
try
{
return repository.Find(id);
}
catch (SqlException ex)
{
throw new UserLoadException($"ユーザー情報の取得に失敗しました。UserId: {id}", ex);
}
}
この例では、データベース関連のSqlExceptionを、業務的な意味を持つUserLoadExceptionに変換しています。その際、元のSqlExceptionをInnerExceptionとして保持しているため、ログを見れば根本原因を追跡できます。
例外を変換するときに元の例外を失うと、原因調査が難しくなります。例外を包み直す場合は、必ずInnerExceptionとして元の例外を渡すようにしましょう。
6. C#の例外設計の考え方
C#のエラー処理では、どこで例外を投げ、どこで捕捉し、どこでユーザー向けメッセージに変換するかを設計することが重要です。
すべてのエラーを例外で扱う必要はありません。例外にすべきものと、戻り値やバリデーションで扱うべきものを分けることで、読みやすく保守しやすいコードになります。
6-1. 例外を使うべきケース
例外を使うべきなのは、メソッドが正常な処理を続けられない異常な状態です。
たとえば、必須の設定ファイルが存在しない、データベース接続に失敗した、外部APIが異常なレスポンスを返した、オブジェクトの状態が不正で処理できない、メソッドの前提条件を満たさない引数が渡された、といったケースです。
C#public void ProcessOrder(Order order)
{
if (order is null)
{
throw new ArgumentNullException(nameof(order));
}
if (order.Items.Count == 0)
{
throw new InvalidOperationException("注文明細が存在しないため処理できません。");
}
// 注文処理
}
例外は、呼び出し元に「この処理は正常に完了できない」と明確に伝えるための仕組みです。
6-2. 例外を使わないほうがよいケース
一方で、通常の業務フローや頻繁に起きる入力ミスには、例外を使わない方がよい場合があります。
たとえば、ログイン失敗、検索結果が0件、ユーザー入力の形式エラー、在庫なし、権限不足などは、アプリケーションによっては通常の分岐として扱うべきです。
C#if (!int.TryParse(input, out int age))
{
return "年齢は数値で入力してください。";
}
このようなケースで例外を多用すると、コードが読みにくくなり、処理の流れが分かりにくくなります。
例外は「異常な状態」を表すために使い、想定内の失敗は戻り値やResult型で表現するのが基本です。
6-3. 戻り値でエラーを表現する方法
戻り値でエラーを表現する最もシンプルな方法は、成功・失敗をboolで返すことです。
C#public bool TryFindUser(int id, out User? user)
{
user = repository.Find(id);
return user != null;
}
呼び出し側は次のように書けます。
C#if (TryFindUser(1, out var user))
{
Console.WriteLine(user.Name);
}
else
{
Console.WriteLine("ユーザーが見つかりません。");
}
この方法は、失敗が通常のケースとして想定される場合に適しています。
ただし、boolだけでは失敗理由を表現しにくいという欠点があります。より詳細なエラー情報が必要な場合は、Result型のような設計を検討します。
6-4. Result型・Tryパターンの活用
Result型とは、処理の成功・失敗と、成功時の値または失敗理由をまとめて表現するための型です。C#標準に汎用的なResult型はありませんが、独自に定義したり、ライブラリを利用したりできます。
簡単な例は次のとおりです。
C#public class Result<T>
{
public bool IsSuccess { get; }
public T? Value { get; }
public string? ErrorMessage { get; }
private Result(bool isSuccess, T? value, string? errorMessage)
{
IsSuccess = isSuccess;
Value = value;
ErrorMessage = errorMessage;
}
public static Result<T> Success(T value)
=> new Result<T>(true, value, null);
public static Result<T> Failure(string errorMessage)
=> new Result<T>(false, default, errorMessage);
}
利用例は次のようになります。
C#public Result<User> CreateUser(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
return Result<User>.Failure("ユーザー名は必須です。");
}
var user = new User(name);
return Result<User>.Success(user);
}
Result型を使うと、入力不備や業務上の失敗を例外ではなく戻り値として扱えます。
C#では、int.TryParseやDateTime.TryParseのようなTryパターンもよく使われます。これは、失敗が十分に想定される処理に向いています。
6-5. カスタム例外クラスを作るべき場面
カスタム例外クラスは、標準の例外クラスでは意味を表現しにくい場合に作ります。
たとえば、業務アプリケーションで「在庫引当エラー」「決済処理エラー」「外部サービス連携エラー」などを明確に区別したい場合です。
C#public class PaymentFailedException : Exception
{
public PaymentFailedException(string message)
: base(message)
{
}
public PaymentFailedException(string message, Exception innerException)
: base(message, innerException)
{
}
}
使い方は次のとおりです。
C#try
{
paymentService.Pay(order);
}
catch (HttpRequestException ex)
{
throw new PaymentFailedException("決済サービスとの通信に失敗しました。", ex);
}
カスタム例外を作るメリットは、呼び出し元で業務的な意味に基づいてcatchできることです。
C#catch (PaymentFailedException ex)
{
Console.WriteLine("決済に失敗しました。");
}
ただし、カスタム例外を作りすぎると管理が難しくなります。既存の例外で十分に意味が伝わる場合は、無理に作る必要はありません。
6-6. 業務アプリケーションでの例外設計例
業務アプリケーションでは、レイヤーごとに例外の扱いを分けると整理しやすくなります。
たとえば、リポジトリ層ではデータベース例外を扱います。アプリケーション層では業務上の失敗として例外を変換します。プレゼンテーション層では、ユーザー向けメッセージに変換します。
C#public class OrderService
{
public void CancelOrder(int orderId)
{
var order = repository.Find(orderId);
if (order == null)
{
throw new OrderNotFoundException($"注文が見つかりません。OrderId: {orderId}");
}
if (order.IsShipped)
{
throw new InvalidOperationException("出荷済みの注文はキャンセルできません。");
}
order.Cancel();
repository.Save(order);
}
}
上位層では、例外の種類に応じてユーザー向けメッセージを返します。
C#try
{
orderService.CancelOrder(orderId);
}
catch (OrderNotFoundException)
{
return NotFound("対象の注文が見つかりません。");
}
catch (InvalidOperationException ex)
{
return BadRequest(ex.Message);
}
catch (Exception ex)
{
logger.LogError(ex, "注文キャンセル中に予期しないエラーが発生しました。");
return StatusCode(500, "システムエラーが発生しました。");
}
このように、内部の例外情報とユーザー向けの応答を分離することが大切です。
7. ログ出力とエラー調査
エラー処理において、ログ出力は非常に重要です。例外を捕捉しても、ログがなければ本番環境で何が起きたのか調査できません。
特にWebアプリケーションやバッチ処理では、ユーザーから「エラーが出ました」と報告されても、ログがなければ原因を特定するのは困難です。
7-1. エラー処理でログが重要な理由
ログが重要な理由は、エラー発生時の状況を後から確認できるからです。
開発環境ではデバッガーで原因を追えますが、本番環境では直接デバッグできないことがほとんどです。そのため、ログに例外情報、処理対象、発生時刻、ユーザーID、リクエストIDなどを残しておく必要があります。
ログが適切に残っていれば、どの処理でエラーが発生したのか、どの入力値が関係していたのか、外部APIやデータベースで何が起きたのかを追跡できます。
逆にログがないと、再現しないエラーを調査できず、同じ問題が何度も発生する可能性があります。
7-2. 何をログに残すべきか
エラー時のログには、原因調査に必要な情報を残します。
具体的には、例外オブジェクト、エラーメッセージ、スタックトレース、発生した処理名、対象データのID、ユーザーID、リクエストID、外部APIのステータスコード、処理時間などが役立ちます。
C#logger.LogError(
ex,
"注文処理中にエラーが発生しました。OrderId: {OrderId}, UserId: {UserId}",
orderId,
userId);
このように、文字列連結ではなく構造化ログとして値を渡すと、ログ検索や集計がしやすくなります。
悪い例は次のようなログです。
C#logger.LogError("エラーが発生しました。");
これだけでは、何が起きたのかほとんど分かりません。ログには、後から調査する人が状況を再現・推測できるだけの情報を残しましょう。
7-3. ログに残してはいけない情報
ログには何でも残せばよいわけではありません。セキュリティや個人情報保護の観点から、記録してはいけない情報があります。
代表的なものは、パスワード、アクセストークン、APIキー、クレジットカード番号、マイナンバー、秘密鍵、認証Cookie、個人を特定できる詳細情報などです。
C#// 悪い例
logger.LogError("ログイン失敗 Password: {Password}", password);
このようなログは非常に危険です。ログは開発者、運用担当者、外部の監視サービスなど複数の場所で参照される可能性があります。
ログに残す値は、調査に必要な最小限にしましょう。ユーザーを特定する必要がある場合でも、メールアドレスではなくユーザーIDを残すなどの工夫が必要です。
7-4. ILoggerを使ったログ出力
ASP.NET Coreなどの.NETアプリケーションでは、ILoggerを使ったログ出力が一般的です。
C#public class UserService
{
private readonly ILogger<UserService> _logger;
public UserService(ILogger<UserService> logger)
{
_logger = logger;
}
public void CreateUser(string name)
{
try
{
// ユーザー作成処理
}
catch (Exception ex)
{
_logger.LogError(ex, "ユーザー作成中にエラーが発生しました。Name: {Name}", name);
throw;
}
}
}
ILoggerでは、ログレベルを使い分けることができます。
TraceやDebugは開発時の詳細情報、Informationは通常の処理状況、Warningは注意が必要だが処理継続可能な状態、Errorは処理失敗、Criticalはアプリケーション全体に影響する重大な障害に使います。
エラー処理では、例外を伴うログはLogErrorで出力することが多いです。アプリケーション停止につながる重大な問題であればLogCriticalを使うこともあります。
7-5. SerilogやNLogなどのログライブラリ活用
標準のILoggerだけでもログ出力はできますが、実務ではSerilogやNLogなどのログライブラリを組み合わせることも多いです。
Serilogは構造化ログに強く、ログをJSON形式で出力したり、ファイル、コンソール、データベース、ログ収集基盤などに送信したりできます。
NLogも柔軟な設定が可能で、出力先やフォーマットを細かく制御できます。
ログライブラリを導入するメリットは、環境ごとにログレベルや出力先を切り替えやすいことです。開発環境ではコンソールに詳細ログを出し、本番環境ではファイルやクラウドのログサービスにエラーログを送る、といった構成ができます。
ただし、ライブラリを使っても、何をログに残すかの設計が不十分であれば意味がありません。ログ出力の場所、ログレベル、メッセージの粒度、機密情報の扱いをチームで決めておくことが重要です。
7-6. 本番環境でのエラー調査の流れ
本番環境でエラーが発生した場合は、まず発生時刻、対象ユーザー、操作内容、エラーメッセージを確認します。次に、アプリケーションログを検索し、該当するリクエストIDや相関IDをもとに関連ログを追跡します。
API通信が関係している場合は、外部APIのステータスコードやレスポンスを確認します。データベースが関係している場合は、タイムアウト、ロック、制約違反、接続数などを確認します。
調査の基本的な流れは、ユーザーから見えた現象、アプリケーションログ、外部サービスログ、インフラメトリクス、データ状態の順に確認することです。
エラー処理とログ設計が適切であれば、原因特定までの時間を大きく短縮できます。逆に、ログが不足していると、推測に頼った調査になり、修正にも時間がかかります。
8. 非同期処理におけるエラー処理
C#では、asyncとawaitを使った非同期処理がよく使われます。非同期処理でも基本的にはtry-catchで例外を捕捉できますが、同期処理とは異なる注意点があります。
8-1. async/awaitでのtry-catch
async/awaitを使う場合も、通常の処理と同じようにtry-catchを書けます。
C#public async Task LoadAsync()
{
try
{
string text = await File.ReadAllTextAsync("data.txt");
Console.WriteLine(text);
}
catch (FileNotFoundException ex)
{
Console.WriteLine("ファイルが見つかりません。");
}
catch (IOException ex)
{
Console.WriteLine("ファイル読み込み中にエラーが発生しました。");
}
}
awaitした非同期処理で例外が発生した場合、その例外はawaitの位置で再スローされるため、周囲のtry-catchで捕捉できます。
API通信でも同様です。
C#try
{
var response = await httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
}
catch (HttpRequestException ex)
{
logger.LogError(ex, "API通信に失敗しました。");
}
8-2. Task内で発生した例外の扱い
Task内で発生した例外は、awaitすることで捕捉できます。
C#try
{
await Task.Run(() =>
{
throw new InvalidOperationException("Task内でエラーが発生しました。");
});
}
catch (InvalidOperationException ex)
{
Console.WriteLine(ex.Message);
}
一方で、awaitしないままTaskを放置すると、例外を適切に扱えないことがあります。
C#Task.Run(() =>
{
throw new InvalidOperationException("エラー");
});
このようなfire-and-forgetの処理は、例外が見逃されやすいため注意が必要です。非同期処理は原則としてawaitし、例外を呼び出し元で扱えるようにしましょう。
どうしてもバックグラウンドで処理する場合は、処理内部で必ずログを出し、失敗時の扱いを明確にしておく必要があります。
8-3. AggregateExceptionとは
AggregateExceptionは、複数の例外をまとめて扱うための例外です。特に、Task.Wait()やTask.Resultを使った場合に見かけることがあります。
C#try
{
Task task = Task.Run(() =>
{
throw new InvalidOperationException("非同期処理でエラー");
});
task.Wait();
}
catch (AggregateException ex)
{
foreach (var inner in ex.InnerExceptions)
{
Console.WriteLine(inner.Message);
}
}
ただし、awaitを使う場合は、多くのケースで元の例外が直接捕捉されます。
C#try
{
await Task.Run(() =>
{
throw new InvalidOperationException("非同期処理でエラー");
});
}
catch (InvalidOperationException ex)
{
Console.WriteLine(ex.Message);
}
現代のC#では、Task.Wait()やTask.Resultよりもawaitを使う方が自然です。デッドロックや例外処理の複雑化を避けるためにも、非同期処理ではasync/awaitを基本にしましょう。
8-4. 非同期処理で例外を握りつぶさない方法
非同期処理では、例外を握りつぶさないことが重要です。
悪い例は次のようなコードです。
C#public async Task ExecuteAsync()
{
try
{
await DoWorkAsync();
}
catch
{
}
}
このコードでは、非同期処理中に何が起きても分かりません。最低限、ログを出す必要があります。
C#public async Task ExecuteAsync()
{
try
{
await DoWorkAsync();
}
catch (Exception ex)
{
logger.LogError(ex, "非同期処理中にエラーが発生しました。");
throw;
}
}
上位層で処理すべき例外であれば、ログを出したうえで再スローします。ここで完全に処理できる例外であれば、ユーザー通知や代替処理を行います。
重要なのは、例外を捕捉したあとに何をするのかを明確にすることです。ログを出す、再試行する、ユーザーへ通知する、処理を中断する、上位へ再スローするなど、方針を決めずにcatchしないようにしましょう。
8-5. CancellationTokenとキャンセル処理
非同期処理では、CancellationTokenを使ってキャンセルを扱うことがあります。
C#public async Task DownloadAsync(CancellationToken cancellationToken)
{
try
{
await httpClient.GetAsync("https://example.com", cancellationToken);
}
catch (OperationCanceledException)
{
Console.WriteLine("処理がキャンセルされました。");
}
}
キャンセル時にはOperationCanceledExceptionやTaskCanceledExceptionが発生することがあります。これは必ずしも異常ではなく、ユーザー操作やタイムアウトによる正常なキャンセルとして扱う場合があります。
そのため、キャンセルとエラーは分けて考える必要があります。
C#try
{
await ExecuteAsync(cancellationToken);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
logger.LogInformation("ユーザー操作により処理がキャンセルされました。");
}
catch (Exception ex)
{
logger.LogError(ex, "処理中にエラーが発生しました。");
throw;
}
キャンセルをエラーとして扱うか、通常の制御として扱うかは、アプリケーションの仕様に応じて決めましょう。
9. やってはいけないC#のエラー処理
C#のエラー処理では、避けるべきアンチパターンがあります。これらは一見便利に見えますが、保守性や安全性を大きく下げる原因になります。
9-1. 空のcatchを書く
最も避けるべきなのが、空のcatchです。
C#try
{
Execute();
}
catch
{
}
このコードでは、例外が発生しても何も起きません。ログも残らず、呼び出し元にも伝わらず、問題が隠れてしまいます。
例外を無視してよいケースは非常に限定的です。仮に無視する場合でも、なぜ無視してよいのかをコメントで明記し、必要に応じてログを残しましょう。
C#try
{
DeleteTemporaryFile(path);
}
catch (IOException ex)
{
logger.LogWarning(ex, "一時ファイルの削除に失敗しました。Path: {Path}", path);
}
9-2. すべての例外をExceptionで捕捉する
catch (Exception ex)は便利ですが、乱用すると危険です。
C#try
{
Execute();
}
catch (Exception ex)
{
Console.WriteLine("エラーが発生しました。");
}
すべての例外を同じように扱うと、復旧可能なエラーと致命的なエラーを区別できません。また、本来は上位層で扱うべき例外まで下位層で握りつぶしてしまうことがあります。
具体的に処理できる例外だけを捕捉し、それ以外は上位へ伝えるのが基本です。
C#try
{
LoadFile();
}
catch (FileNotFoundException ex)
{
logger.LogWarning(ex, "ファイルが見つかりません。");
}
catch (IOException ex)
{
logger.LogError(ex, "ファイル読み込み中にエラーが発生しました。");
throw;
}
アプリケーションの最上位層では、予期しない例外をまとめて捕捉してログを出すことがあります。ただし、それは最後の安全網として設計すべきです。
9-3. 例外をログだけ出して無視する
例外をログに出すだけで、その後何もせず処理を続けるのも危険です。
C#try
{
SaveData();
}
catch (Exception ex)
{
logger.LogError(ex, "保存に失敗しました。");
}
このコードでは、保存に失敗しているにもかかわらず、呼び出し元は成功したかのように処理を続ける可能性があります。結果として、ユーザーに誤った成功メッセージを表示したり、後続処理でデータ不整合が起きたりします。
例外を捕捉したら、ログ出力だけでなく、再スロー、戻り値で失敗を返す、ユーザーへ通知する、処理を中断するなど、明確な対応が必要です。
C#catch (Exception ex)
{
logger.LogError(ex, "保存に失敗しました。");
throw;
}
9-4. 例外を通常の分岐処理に使う
例外は、通常の分岐処理の代わりに使うべきではありません。
悪い例は次のようなコードです。
C#try
{
int number = int.Parse(input);
Console.WriteLine(number);
}
catch (FormatException)
{
Console.WriteLine("数値ではありません。");
}
このコード自体は間違いではありませんが、ユーザー入力のように失敗が頻繁に起きる処理ではTryParseの方が適しています。
C#if (int.TryParse(input, out int number))
{
Console.WriteLine(number);
}
else
{
Console.WriteLine("数値ではありません。");
}
例外は、通常のフローから外れた異常を表現するためのものです。入力チェックや検索結果なしのような想定内のケースは、戻り値や条件分岐で扱う方が読みやすくなります。
9-5. ユーザーに内部エラーをそのまま表示する
例外メッセージやスタックトレースをユーザーにそのまま表示するのは危険です。
C#catch (Exception ex)
{
return ex.ToString();
}
このような実装では、内部クラス名、ファイルパス、SQL文、サーバー構成などがユーザーに見えてしまう可能性があります。
ユーザーには分かりやすく安全なメッセージを表示し、詳細はログに残しましょう。
C#catch (Exception ex)
{
logger.LogError(ex, "予期しないエラーが発生しました。");
return "システムエラーが発生しました。時間をおいて再度お試しください。";
}
開発者向けの情報とユーザー向けの情報を分離することは、C#のエラー処理における重要な基本です。
10. C#のエラー処理ベストプラクティス
ここでは、C#でエラー処理を実装するときに意識したいベストプラクティスをまとめます。
10-1. 具体的な例外型をcatchする
例外を捕捉するときは、できるだけ具体的な例外型を指定しましょう。
C#catch (FileNotFoundException ex)
{
// ファイルがない場合の処理
}
catch (UnauthorizedAccessException ex)
{
// 権限がない場合の処理
}
具体的な例外型を使うことで、エラーの種類に応じた適切な対応ができます。すべてをExceptionで捕捉すると、処理方針が曖昧になります。
ただし、アプリケーションの境界や最上位層では、予期しない例外をログに残すためにExceptionを捕捉することがあります。その場合でも、握りつぶさずに適切なレスポンスや終了処理を行いましょう。
10-2. 必要な場所だけでtry-catchする
try-catchは、必要な場所だけに書きます。すべてのメソッドを機械的にtry-catchで囲む必要はありません。
悪い例は次のようなコードです。
C#public void MethodA()
{
try
{
MethodB();
}
catch (Exception ex)
{
throw;
}
}
このように何も処理せず再スローするだけなら、そのtry-catchは不要です。
try-catchを書くべきなのは、例外に対して具体的な対応をする場所です。ログを出す、別の例外に変換する、リソースを解放する、ユーザー向けメッセージに変換する、再試行するなど、明確な目的がある場合に使いましょう。
10-3. 例外の原因を失わない
例外を再スローするときは、原因を失わないようにします。
C#catch (Exception ex)
{
logger.LogError(ex, "エラーが発生しました。");
throw;
}
throw;を使えば、元のスタックトレースを保持できます。
例外を包み直す場合は、InnerExceptionに元の例外を渡します。
C#catch (IOException ex)
{
throw new InvalidOperationException("ファイル読み込みに失敗しました。", ex);
}
原因を失うと、ログを見ても本当の発生箇所が分からなくなります。エラー調査のしやすさを考えて、スタックトレースとInnerExceptionを保持しましょう。
10-4. ユーザー向けメッセージと開発者向けログを分ける
ユーザーに表示するメッセージと、開発者が調査するためのログは分けるべきです。
ユーザー向けには、専門用語を避け、次に何をすればよいか分かるメッセージにします。
ファイルを読み込めませんでした。ファイルが存在するか確認してください。
開発者向けログには、例外情報や処理対象IDなど、原因調査に必要な情報を残します。
C#logger.LogError(ex, "ファイル読み込みに失敗しました。Path: {Path}", path);
内部エラーをそのままユーザーに表示すると、混乱を招くだけでなく、セキュリティ上のリスクにもなります。
10-5. 復旧可能なエラーと致命的エラーを分ける
エラーには、復旧可能なものと致命的なものがあります。
一時的なネットワーク障害であれば再試行できるかもしれません。ユーザー入力の誤りであれば、再入力を促せば復旧できます。ファイルが見つからない場合は、別のファイルを選ばせることができます。
一方で、必須設定が壊れている、データベースに接続できない、アプリケーションの前提条件が崩れているといった場合は、処理を継続しない方が安全です。
復旧可能なエラーでは、再試行、代替処理、ユーザーへの修正依頼を行います。致命的なエラーでは、ログを残して安全に処理を中断します。
すべてのエラーを同じように扱わず、エラーの性質に応じて対応を分けることが重要です。
10-6. チームで例外処理ルールを統一する
実務では、チーム内で例外処理のルールを統一することが大切です。
たとえば、どの層でログを出すのか、どの層で例外を変換するのか、カスタム例外を作る基準は何か、ユーザー向けメッセージはどこで生成するのか、例外を握りつぶしてよいケースはあるのか、といったルールを決めておきます。
ルールがないと、あるメソッドではログを出し、別のメソッドでは出さず、さらに別の場所では例外を握りつぶす、といったばらつきが生まれます。
例外処理は、コード品質だけでなく運用品質にも直結します。チームで方針を共有し、レビューでも確認するようにしましょう。
11. C#のエラー処理に関するよくある質問
ここでは、C#のエラー処理でよくある疑問に回答します。
11-1. try-catchはどこに書くべき?
try-catchは、例外に対して具体的な対応ができる場所に書くべきです。
たとえば、ファイルがなければユーザーに再選択を促せる画面層、API通信に失敗したら再試行できるサービス層、予期しない例外をログに残して500エラーへ変換するWebアプリの境界などです。
逆に、何も処理せずにthrow;するだけのtry-catchは基本的に不要です。すべてのメソッドに機械的にtry-catchを書くのではなく、その場所で何ができるかを考えて配置しましょう。
11-2. finallyとusingはどちらを使うべき?
ファイル、ストリーム、データベース接続などのリソース解放が目的なら、基本的にはusingを使うのがおすすめです。
C#using var stream = new FileStream("sample.txt", FileMode.Open);
usingを使うと、スコープを抜けるときに自動でDisposeが呼ばれます。コードも簡潔になります。
一方、finallyは、リソース解放以外の後処理にも使えます。たとえば、一時ファイルの削除、状態の復元、ロック解除、処理終了ログなどです。
単純なIDisposableの解放ならusing、例外の有無にかかわらず任意の後処理をしたいならfinallyと考えるとよいでしょう。
11-3. 例外処理はパフォーマンスに影響する?
例外を投げる処理は、通常の条件分岐に比べてコストが高いです。そのため、頻繁に発生する通常の分岐に例外を使うのは避けるべきです。
たとえば、ユーザー入力の数値変換では、int.Parseをtry-catchで囲むよりint.TryParseを使う方が適しています。
C#if (int.TryParse(input, out int number))
{
// 成功
}
else
{
// 失敗
}
ただし、例外が本当に異常系でしか発生しないのであれば、過度にパフォーマンスを心配する必要はありません。問題なのは、例外を通常の制御フローとして大量に発生させる設計です。
11-4. catchしない例外はどうなる?
catchされなかった例外は、呼び出し元へ伝播します。呼び出し元でも捕捉されなければ、さらに上位へ伝わり、最終的にはアプリケーションの未処理例外として扱われます。
コンソールアプリやデスクトップアプリでは、未処理例外によってアプリケーションが終了することがあります。Webアプリケーションでは、フレームワークの例外処理ミドルウェアなどで捕捉され、500エラーとして返されることがあります。
そのため、アプリケーションの境界には、未処理例外をログに残す仕組みを用意しておくことが重要です。ただし、すべての例外を下位層で捕捉する必要はありません。適切な層まで伝播させ、そこでまとめて処理する方がよい場合もあります。
11-5. カスタム例外は必ず作るべき?
カスタム例外は必ず作る必要はありません。標準の例外クラスで意味が十分に表現できるなら、標準例外を使う方がシンプルです。
たとえば、引数が不正ならArgumentException、引数がnullならArgumentNullException、現在の状態では操作できないならInvalidOperationExceptionで十分なケースが多いです。
カスタム例外を作るべきなのは、業務的な意味を明確に表したい場合や、呼び出し元で特定のエラーだけを捕捉したい場合です。
C#catch (PaymentFailedException)
{
// 決済失敗時の処理
}
カスタム例外を作る場合は、名前、用途、発生条件をチームで統一しましょう。むやみに増やすと、かえって例外設計が複雑になります。
まとめ
C#のエラー処理は、安定したアプリケーションを作るために欠かせない重要な要素です。基本となるのはtry-catch-finallyですが、単に例外を捕捉するだけでは不十分です。どの例外を捕捉するのか、どこでログを出すのか、例外を再スローするのか、ユーザーにはどのようなメッセージを表示するのかを設計する必要があります。
NullReferenceException、ArgumentException、InvalidOperationException、FormatException、IOExceptionなどの代表的な例外クラスを理解しておくと、状況に応じた適切なエラー処理を書きやすくなります。
また、throw new Exceptionやthrow ex、空のcatch、例外の握りつぶし、ユーザーへの内部エラー表示などは避けるべきです。例外の原因を失わず、ログには調査に必要な情報を残し、機密情報は出力しないようにしましょう。
非同期処理では、async/awaitとtry-catchを組み合わせて例外を捕捉し、Taskの例外やキャンセル処理も適切に扱う必要があります。
C#のエラー処理で大切なのは、例外を「発生したらとりあえずcatchするもの」と考えるのではなく、「アプリケーションを安全に動かし、問題発生時に原因を追跡できるようにする設計」として考えることです。復旧可能なエラーと致命的なエラーを分け、ユーザー向けメッセージと開発者向けログを分離し、チームでルールを統一することで、保守性と信頼性の高いC#アプリケーションを実現できます。

