[C#] await中に発生する例外のキャッチ・処理方法
C#でawait
中に例外が発生した場合、その例外は非同期タスクが完了した時点でスローされます。
例外をキャッチするには、await
を含むコードをtry-catch
ブロックで囲む必要があります。
await
は非同期メソッドの完了を待機するため、例外はその時点でキャッチされます。
例えば、await
で呼び出されたメソッドがTask
やTask<T>
を返す場合、そのタスクが失敗すると例外がスローされ、catch
ブロックで処理できます。
- await中の例外処理の方法
- TaskとTask<T>の違い
- AggregateExceptionの処理方法
- 非同期メソッド設計のベストプラクティス
- 複数タスクの例外処理の応用例
await中に発生する例外のキャッチ方法
C#の非同期プログラミングにおいて、await
を使用する際に発生する例外のキャッチ方法について解説します。
非同期メソッド内での例外処理は、通常の同期メソッドとは異なる点が多いため、注意が必要です。
以下に、具体的な例を交えながら説明します。
try-catchブロックでの例外処理
非同期メソッド内で例外をキャッチするためには、try-catch
ブロックを使用します。
以下はその例です。
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
try
{
await ExampleAsync(); // 非同期メソッドを呼び出す
}
catch (Exception ex)
{
Console.WriteLine($"例外が発生しました: {ex.Message}"); // 例外メッセージを表示
}
}
static async Task ExampleAsync()
{
await Task.Delay(1000); // 1秒待機
throw new InvalidOperationException("無効な操作です"); // 例外をスロー
}
}
例外が発生しました: 無効な操作です
このコードでは、ExampleAsyncメソッド
内で例外がスローされ、try-catch
ブロックによってキャッチされます。
TaskとTask<T>の例外処理の違い
Task
とTask<T>
の例外処理にはいくつかの違いがあります。
以下の表にまとめます。
特徴 | Task | Task<T> |
---|---|---|
戻り値 | なし | T型 の戻り値 |
例外のキャッチ方法 | try-catch でキャッチ可能 | try-catch でキャッチ可能 |
例外の取得方法 | Task.Exception で取得可能 | Task<T>.Exception で取得可能 |
Task<T>
の場合、戻り値があるため、例外が発生した場合はResult
プロパティを参照することができません。
例外はTask<T>.Exception
を通じて取得します。
AggregateExceptionの扱い方
複数の例外が発生した場合、Task
はAggregateException
をスローします。
この例外は、複数の例外をまとめて管理するためのクラスです。
以下はその例です。
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
try
{
await Task.WhenAll(Task1(), Task2()); // 複数のタスクを同時に実行
}
catch (AggregateException ex)
{
foreach (var innerEx in ex.InnerExceptions)
{
Console.WriteLine($"例外が発生しました: {innerEx.Message}"); // 各例外メッセージを表示
}
}
}
static async Task Task1()
{
await Task.Delay(1000);
throw new InvalidOperationException("Task1の例外"); // 例外をスロー
}
static async Task Task2()
{
await Task.Delay(1000);
throw new NullReferenceException("Task2の例外"); // 例外をスロー
}
}
例外が発生しました: Task1の例外
例外が発生しました: Task2の例外
このコードでは、Task.WhenAll
を使用して複数のタスクを実行し、AggregateException
を通じて各例外をキャッチしています。
awaitでの例外再スローの仕組み
await
を使用する際、例外が発生すると、その例外は呼び出し元に再スローされます。
これにより、非同期メソッド内で発生した例外を、呼び出し元でキャッチすることができます。
以下はその例です。
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
try
{
await ExampleAsync(); // 非同期メソッドを呼び出す
}
catch (Exception ex)
{
Console.WriteLine($"例外が発生しました: {ex.Message}"); // 例外メッセージを表示
}
}
static async Task ExampleAsync()
{
await Task.Delay(1000); // 1秒待機
throw new InvalidOperationException("無効な操作です"); // 例外をスロー
}
}
例外が発生しました: 無効な操作です
このように、await
を使用することで、非同期メソッド内で発生した例外を呼び出し元で簡単にキャッチできます。
非同期メソッド内での例外の伝播
非同期メソッド内で発生した例外は、呼び出し元のメソッドに伝播します。
これにより、非同期メソッドの呼び出し元で例外を処理することが可能です。
以下はその例です。
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
try
{
await ExampleAsync(); // 非同期メソッドを呼び出す
}
catch (Exception ex)
{
Console.WriteLine($"例外が発生しました: {ex.Message}"); // 例外メッセージを表示
}
}
static async Task ExampleAsync()
{
await Task.Delay(1000); // 1秒待機
throw new InvalidOperationException("無効な操作です"); // 例外をスロー
}
}
例外が発生しました: 無効な操作です
このコードでは、ExampleAsyncメソッド
内で発生した例外がMainメソッド
に伝播し、そこでキャッチされています。
非同期メソッド内での例外処理は、呼び出し元での処理を考慮することが重要です。
例外処理のベストプラクティス
非同期プログラミングにおける例外処理は、アプリケーションの安定性と信頼性を確保するために非常に重要です。
以下に、非同期メソッドでの例外処理に関するベストプラクティスを解説します。
非同期メソッドでの例外処理の設計
非同期メソッドを設計する際は、例外処理を考慮することが重要です。
以下のポイントを押さえておきましょう。
- 明示的な例外処理: 各非同期メソッド内で
try-catch
ブロックを使用し、例外を適切にキャッチする。 - 例外の再スロー: 必要に応じて、例外を再スローして呼び出し元で処理できるようにする。
- エラーメッセージの明確化: 例外メッセージは具体的でわかりやすいものにする。
ConfigureAwait(false)の使用と例外処理
ConfigureAwait(false)
を使用することで、コンテキストを捕捉せずに非同期処理を実行できます。
これにより、UIスレッドのブロックを避けることができますが、例外処理にも影響があります。
- UIスレッドのブロック回避: UIアプリケーションでは、
ConfigureAwait(false)
を使用することで、UIスレッドをブロックせずに非同期処理を行う。 - 例外のキャッチ:
ConfigureAwait(false)
を使用した場合でも、例外は呼び出し元でキャッチ可能です。
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
try
{
await ExampleAsync().ConfigureAwait(false); // UIスレッドをブロックしない
}
catch (Exception ex)
{
Console.WriteLine($"例外が発生しました: {ex.Message}"); // 例外メッセージを表示
}
}
static async Task ExampleAsync()
{
await Task.Delay(1000); // 1秒待機
throw new InvalidOperationException("無効な操作です"); // 例外をスロー
}
}
例外が発生しました: 無効な操作です
Task.WhenAllやTask.WhenAnyでの例外処理
複数のタスクを同時に実行する場合、Task.WhenAll
やTask.WhenAny
を使用しますが、例外処理には注意が必要です。
Task.WhenAll
: 複数のタスクがすべて完了するまで待機し、いずれかのタスクが失敗した場合はAggregateException
をスローします。Task.WhenAny
: 最初に完了したタスクを取得し、他のタスクの結果や例外は無視されます。
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
try
{
await Task.WhenAll(Task1(), Task2()); // 複数のタスクを同時に実行
}
catch (AggregateException ex)
{
foreach (var innerEx in ex.InnerExceptions)
{
Console.WriteLine($"例外が発生しました: {innerEx.Message}"); // 各例外メッセージを表示
}
}
}
static async Task Task1()
{
await Task.Delay(1000);
throw new InvalidOperationException("Task1の例外"); // 例外をスロー
}
static async Task Task2()
{
await Task.Delay(1000);
throw new NullReferenceException("Task2の例外"); // 例外をスロー
}
}
例外が発生しました: Task1の例外
例外が発生しました: Task2の例外
Taskの状態確認と例外処理
タスクの状態を確認することで、例外が発生したかどうかを判断できます。
Task.Status
プロパティを使用して、タスクの状態を確認しましょう。
- 状態確認:
Task.Status
を使用して、タスクが完了したか、失敗したかを確認する。 - 例外の取得: タスクが失敗した場合は、
Task.Exception
を使用して例外を取得する。
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
Task task = ExampleAsync(); // 非同期メソッドを呼び出す
await task; // タスクの完了を待機
if (task.IsFaulted) // タスクが失敗したか確認
{
Console.WriteLine($"例外が発生しました: {task.Exception.InnerException.Message}"); // 例外メッセージを表示
}
}
static async Task ExampleAsync()
{
await Task.Delay(1000); // 1秒待機
throw new InvalidOperationException("無効な操作です"); // 例外をスロー
}
}
例外が発生しました: 無効な操作です
ログ出力と例外処理の組み合わせ
例外が発生した際には、適切なログ出力を行うことが重要です。
これにより、問題の診断やトラブルシューティングが容易になります。
- ログライブラリの使用:
Serilog
やNLog
などのログライブラリを使用して、例外情報をログに記録する。 - 例外の詳細情報: 例外メッセージだけでなく、スタックトレースや発生時のコンテキスト情報も記録する。
using System;
using System.Threading.Tasks;
using Serilog; // Serilogライブラリを使用
class Program
{
static async Task Main(string[] args)
{
Log.Logger = new LoggerConfiguration()
.WriteTo.Console() // コンソールにログを出力
.CreateLogger();
try
{
await ExampleAsync(); // 非同期メソッドを呼び出す
}
catch (Exception ex)
{
Log.Error(ex, "例外が発生しました"); // 例外をログに記録
}
}
static async Task ExampleAsync()
{
await Task.Delay(1000); // 1秒待機
throw new InvalidOperationException("無効な操作です"); // 例外をスロー
}
}
[エラーログ] 例外が発生しました: 無効な操作です
このように、例外処理とログ出力を組み合わせることで、アプリケーションの信頼性を向上させることができます。
応用例:複数の非同期タスクでの例外処理
非同期プログラミングでは、複数のタスクを同時に実行することが一般的です。
ここでは、複数の非同期タスクでの例外処理の応用例を解説します。
複数のawaitを使った例外処理
複数のawait
を使用する場合、各非同期メソッド内で例外を個別にキャッチすることができます。
以下はその例です。
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
try
{
await Task1(); // 最初の非同期メソッドを呼び出す
}
catch (Exception ex)
{
Console.WriteLine($"Task1で例外が発生しました: {ex.Message}"); // 例外メッセージを表示
}
try
{
await Task2(); // 次の非同期メソッドを呼び出す
}
catch (Exception ex)
{
Console.WriteLine($"Task2で例外が発生しました: {ex.Message}"); // 例外メッセージを表示
}
}
static async Task Task1()
{
await Task.Delay(1000); // 1秒待機
throw new InvalidOperationException("Task1の無効な操作です"); // 例外をスロー
}
static async Task Task2()
{
await Task.Delay(1000); // 1秒待機
throw new NullReferenceException("Task2の参照が無効です"); // 例外をスロー
}
}
Task1で例外が発生しました: Task1の無効な操作です
Task2で例外が発生しました: Task2の参照が無効です
このコードでは、各タスクの例外を個別にキャッチし、メッセージを表示しています。
Task.WhenAllでの例外キャッチ
複数のタスクを同時に実行する場合、Task.WhenAll
を使用すると、すべてのタスクが完了するまで待機し、いずれかのタスクが失敗した場合はAggregateException
をスローします。
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
try
{
await Task.WhenAll(Task1(), Task2()); // 複数のタスクを同時に実行
}
catch (AggregateException ex)
{
foreach (var innerEx in ex.InnerExceptions)
{
Console.WriteLine($"例外が発生しました: {innerEx.Message}"); // 各例外メッセージを表示
}
}
}
static async Task Task1()
{
await Task.Delay(1000); // 1秒待機
throw new InvalidOperationException("Task1の無効な操作です"); // 例外をスロー
}
static async Task Task2()
{
await Task.Delay(1000); // 1秒待機
throw new NullReferenceException("Task2の参照が無効です"); // 例外をスロー
}
}
例外が発生しました: Task1の無効な操作です
例外が発生しました: Task2の参照が無効です
このように、Task.WhenAll
を使用することで、複数のタスクの例外を一度にキャッチできます。
Task.WhenAnyでの例外キャッチ
Task.WhenAny
を使用すると、最初に完了したタスクを取得し、他のタスクの結果や例外は無視されます。
以下はその例です。
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
Task task1 = Task1(); // 最初のタスクを開始
Task task2 = Task2(); // 次のタスクを開始
Task completedTask = await Task.WhenAny(task1, task2); // 最初に完了したタスクを取得
if (completedTask.IsFaulted) // 完了したタスクが失敗したか確認
{
Console.WriteLine($"例外が発生しました: {completedTask.Exception.InnerException.Message}"); // 例外メッセージを表示
}
else
{
Console.WriteLine("タスクが正常に完了しました。"); // 正常完了メッセージを表示
}
}
static async Task Task1()
{
await Task.Delay(1000); // 1秒待機
throw new InvalidOperationException("Task1の無効な操作です"); // 例外をスロー
}
static async Task Task2()
{
await Task.Delay(500); // 0.5秒待機
// 正常に完了
}
}
タスクが正常に完了しました。
このコードでは、Task2
が先に完了し、Task1
の例外は無視されます。
並列処理と例外処理の組み合わせ
並列処理を行う際には、例外処理を適切に組み合わせることが重要です。
以下の例では、複数のタスクを並列に実行し、例外をキャッチしています。
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
var tasks = new[] { Task1(), Task2() }; // タスクの配列を作成
try
{
await Task.WhenAll(tasks); // すべてのタスクを同時に実行
}
catch (AggregateException ex)
{
foreach (var innerEx in ex.InnerExceptions)
{
Console.WriteLine($"例外が発生しました: {innerEx.Message}"); // 各例外メッセージを表示
}
}
}
static async Task Task1()
{
await Task.Delay(1000); // 1秒待機
throw new InvalidOperationException("Task1の無効な操作です"); // 例外をスロー
}
static async Task Task2()
{
await Task.Delay(1000); // 1秒待機
throw new NullReferenceException("Task2の参照が無効です"); // 例外をスロー
}
}
例外が発生しました: Task1の無効な操作です
例外が発生しました: Task2の参照が無効です
このように、並列処理を行う際には、Task.WhenAll
を使用して例外を一括でキャッチすることができます。
非同期ストリーム(IAsyncEnumerable)での例外処理
非同期ストリームを使用する場合、IAsyncEnumerable<T>
を利用して、非同期にデータを列挙することができます。
例外処理も同様に行えます。
以下はその例です。
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
await foreach (var item in GetItemsAsync()) // 非同期ストリームを列挙
{
Console.WriteLine(item); // アイテムを表示
}
}
static async IAsyncEnumerable<string> GetItemsAsync()
{
for (int i = 0; i < 5; i++)
{
await Task.Delay(500); // 0.5秒待機
if (i == 3) throw new InvalidOperationException("無効な操作です"); // 例外をスロー
yield return $"アイテム {i}"; // アイテムを返す
}
}
}
アイテム 0
アイテム 1
アイテム 2
このコードでは、GetItemsAsyncメソッド
内で例外が発生しますが、await foreach
の外でキャッチされるため、例外処理を行うことができます。
非同期ストリームを使用する際は、例外が発生した場合の処理を考慮することが重要です。
よくある質問
まとめ
この記事では、C#における非同期プログラミングにおける例外処理の重要性とその具体的な方法について詳しく解説しました。
特に、await
中に発生する例外のキャッチ方法や、Task
とTask<T>
の違い、AggregateException
の処理方法など、実践的な知識を提供しました。
非同期処理を行う際には、例外処理を適切に設計し、実装することが重要ですので、ぜひこれらのテクニックを活用して、より堅牢なアプリケーションを構築してみてください。