[C#] await中に発生する例外のキャッチ・処理方法

C#でawait中に例外が発生した場合、その例外は非同期タスクが完了した時点でスローされます。

例外をキャッチするには、awaitを含むコードをtry-catchブロックで囲む必要があります。

awaitは非同期メソッドの完了を待機するため、例外はその時点でキャッチされます。

例えば、awaitで呼び出されたメソッドがTaskTask<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>の例外処理の違い

TaskTask<T>の例外処理にはいくつかの違いがあります。

以下の表にまとめます。

スクロールできます
特徴TaskTask<T>
戻り値なしT型の戻り値
例外のキャッチ方法try-catchでキャッチ可能try-catchでキャッチ可能
例外の取得方法Task.Exceptionで取得可能Task<T>.Exceptionで取得可能

Task<T>の場合、戻り値があるため、例外が発生した場合はResultプロパティを参照することができません。

例外はTask<T>.Exceptionを通じて取得します。

AggregateExceptionの扱い方

複数の例外が発生した場合、TaskAggregateExceptionをスローします。

この例外は、複数の例外をまとめて管理するためのクラスです。

以下はその例です。

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.WhenAllTask.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("無効な操作です"); // 例外をスロー
    }
}
例外が発生しました: 無効な操作です

ログ出力と例外処理の組み合わせ

例外が発生した際には、適切なログ出力を行うことが重要です。

これにより、問題の診断やトラブルシューティングが容易になります。

  • ログライブラリの使用: SerilogNLogなどのログライブラリを使用して、例外情報をログに記録する。
  • 例外の詳細情報: 例外メッセージだけでなく、スタックトレースや発生時のコンテキスト情報も記録する。
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の外でキャッチされるため、例外処理を行うことができます。

非同期ストリームを使用する際は、例外が発生した場合の処理を考慮することが重要です。

よくある質問

await中に例外が発生した場合、どのタイミングでキャッチされる?

await中に例外が発生した場合、その例外はawaitが完了した後に呼び出し元に再スローされます。

具体的には、非同期メソッド内でawaitを使用しているときに例外が発生すると、その例外は呼び出し元のtry-catchブロックでキャッチされます。

これにより、非同期メソッド内で発生した例外を、呼び出し元で適切に処理することが可能です。

Taskの例外とTask<T>の例外はどう違う?

TaskTask<T>の主な違いは、戻り値の有無です。

Taskは戻り値を持たない非同期処理を表し、Task<T>は型Tの戻り値を持つ非同期処理を表します。

例外処理に関しては、以下の点が異なります。

  • Task: 例外が発生した場合、Task.Exceptionプロパティを使用して例外を取得できます。
  • Task<T>: 例外が発生した場合、Task<T>.Exceptionを使用して例外を取得し、Resultプロパティを参照するとInvalidOperationExceptionがスローされます。

したがって、例外が発生した場合は、Resultを参照する前にTask<T>.Exceptionを確認する必要があります。

AggregateExceptionはどのように処理すればよい?

AggregateExceptionは、複数の例外が同時に発生した場合にスローされる例外です。

主にTask.WhenAllTask.WaitAllを使用した際に発生します。

AggregateExceptionを処理する際は、以下の手順を踏むことが一般的です。

  1. try-catchブロックでAggregateExceptionをキャッチします。
  2. InnerExceptionsプロパティを使用して、発生したすべての例外を取得します。
  3. 各例外に対して適切な処理を行います。

例えば、ログ出力やユーザーへの通知などです。

以下はその例です。

try
{
    await Task.WhenAll(Task1(), Task2()); // 複数のタスクを同時に実行
}
catch (AggregateException ex)
{
    foreach (var innerEx in ex.InnerExceptions)
    {
        Console.WriteLine($"例外が発生しました: {innerEx.Message}"); // 各例外メッセージを表示
    }
}

このように、AggregateExceptionを適切に処理することで、複数の例外を一括で管理し、アプリケーションの安定性を向上させることができます。

まとめ

この記事では、C#における非同期プログラミングにおける例外処理の重要性とその具体的な方法について詳しく解説しました。

特に、await中に発生する例外のキャッチ方法や、TaskTask<T>の違い、AggregateExceptionの処理方法など、実践的な知識を提供しました。

非同期処理を行う際には、例外処理を適切に設計し、実装することが重要ですので、ぜひこれらのテクニックを活用して、より堅牢なアプリケーションを構築してみてください。

当サイトはリンクフリーです。出典元を明記していただければ、ご自由に引用していただいて構いません。

関連カテゴリーから探す

  • URLをコピーしました!
目次から探す