CodeGym /コース /C# SELF /非同期コードでの例外処理

非同期コードでの例外処理

C# SELF
レベル 59 , レッスン 4
使用可能

1. 非同期メソッドと例外

コードで何か悪いこと(例えばゼロ除算や存在しないファイルへのアクセス)が起きると例外が投げられ、通常は try-catch で捕まえます。順次かつ単一スレッドで実行される間はそれで簡単です。しかし非同期が入ると状況は宇宙空間みたいになり、例外は期待している場所とは遠くで「発生」したり、まったく見逃されたりします。

理由は、非同期メソッドはたいていタスク(Task)を返し、メソッドから抜けた後も実行が続くことがあるからです。例外はメインスレッドが「実行を手放した」後に発生するかもしれません。だから非同期メソッドの呼び出しを囲む従来の try-catch は、同期コードのときと同じようには機能しないことがあります。

簡単な例で見てみましょう。ミニアプリに次のような非同期メソッドがあるとします:

// フラグメント: 非同期の「レポート送信」計算
public async Task SendReportAsync()
{
    // ここにはネットワーク呼び出しやファイルアクセスがあるかもしれない
    await Task.Delay(100);
    throw new InvalidOperationException("レポート送信中のエラー!");
}

呼び出し方はこうです:

SendReportAsync();
Console.WriteLine("処理を続けます...");

ビジュアル化

flowchart TD
    Start["メイン スレッド"]
    Call[/"SendReportAsync() の呼び出し"/]
    Continue["処理は続く..."]
    Exception["Task 内で例外が発生"]
    Unhandled["例外が未処理のまま!"]
    Start --> Call --> Continue
    Call -.- Exception --> Unhandled

結論: 非同期メソッドが Task を返し、タスクの完了を待たない(await.Wait() を使わない)と、例外は「見逃され」ます。最良の場合はランタイムが "Task 内の未処理例外" 的なログを出します。最悪の場合はエラーを完全に見失い、原因不明のバグを長時間探す羽目になります。

2. 非同期コードで例外を正しく捕まえるには?

await + try-catch を使う

正しいやり方を見てみましょう:

try
{
    await SendReportAsync(); // Task の完了を待つ
    Console.WriteLine("レポートは正常に送信されました!");
}
catch (Exception ex)
{
    Console.WriteLine($"おっと! 何か問題が発生しました: {ex.Message}");
}

どう動くか? 呼び出しの前に await を置くと、C# はあなたのメソッドを await の前後で分割します。非同期部分で例外が起きると、それは await を置いた場所で再スローされ、通常の try-catch で捕まえられます。

アプリ向けの例

デモにエラーハンドリングを追加します:

public async Task StartReportProcessAsync()
{
    try
    {
        await SendReportAsync();
        Console.WriteLine("レポートは正常に送信されました!");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"レポート送信中のエラー: {ex.Message}");
    }
}

呼び出し:

await StartReportProcessAsync();

.Wait(), .Result — コンソールでは最悪ではないが推奨されない手段

特にコンソールアプリでは、トップレベルで await を使えないことがあります(古い C#、Main メソッド)。その場合は同期的にタスクを待つために .Wait().Result を使うことになります。

try
{
    SendReportAsync().Wait();
}
catch (AggregateException aggEx)
{
    foreach (var ex in aggEx.InnerExceptions)
        Console.WriteLine($"エラー: {ex.Message}");
}

なぜか? .Wait().Result は元の例外を常に AggregateException でラップします。これは一つ以上の例外を含められるコンテナです。内部に一つまたは複数の例外があるかもしれないので、ループで分解する必要があります。AggregateException の詳細は 公式ドキュメント を参照してください。

重要!

最新の .NET(C# 7.1 以降)では、非同期の Main を宣言してエントリポイントで直接 await を使えます:

static async Task Main(string[] args)
{
    await StartReportProcessAsync();
}

3. "fire-and-forget" タスクの例外

非同期メソッドを起動して完了を待たず、タスクへの参照も保持しなかったら何が起きるでしょう?

SendReportAsync(); // タスクを「忘れた」

この場合の問題は、タスク内で発生した例外が誰にも処理されないことです。環境や設定によってはアプリがクラッシュすることもあれば、単に警告がログに残るだけのこともあります。これは C# のバグではなく、タスクの動作の論理的帰結です。

正しいやり方は?

  • 理想的には、タスクが重大な失敗で終了する可能性があるなら "fire-and-forget" を避ける。
  • どうしても "fire-and-forget" にするなら、メソッド内部で明示的にエラー処理を行う。
public async Task SendReportSafeAsync()
{
    try
    {
        await Task.Delay(100);
        throw new InvalidOperationException("送信中のエラー!");
    }
    catch (Exception ex)
    {
        // ログ記録やエラーハンドリング
        Console.WriteLine($"[ログ] 例外: {ex.Message}");
    }
}

// 呼び出し
SendReportSafeAsync();

要点: タスクを追跡せず await もしない場合、非同期メソッドの本体を必ず try-catch で包み、少なくともログを残すようにしましょう。そうすればエラーを失わずに済みます。

4. 並列タスクと例外: Task.WhenAll

実アプリでは複数の独立した非同期タスクを同時に走らせ、すべての完了を待ちたいことがよくあります。例えば複数の宛先に並列でレポートを送る場合:

var tasks = new List<Task>
{
    SendReportAsync(),
    SendReportAsync(),
    SendReportAsync()
};

await Task.WhenAll(tasks);

1つ以上のタスクが例外を投げたら何が起きるか?

こうやって捕まえる

awaitTask.WhenAll(tasks) を待つと、少なくとも一つのタスクがエラーで終了していれば、await は最初にエラーで終わったタスクの例外を再スローします(通常は AggregateException でラップされません)。
ただし注意: 複数のタスクが失敗している場合、結果として AggregateException が投げられ、内部に複数の例外が含まれることがあります。

try
{
    await Task.WhenAll(tasks);
}
catch (Exception ex)
{
    // もし AggregateException なら分解する
    if (ex is AggregateException agg)
    {
        foreach (var inner in agg.InnerExceptions)
            Console.WriteLine($"タスク内のエラー: {inner.Message}");
    }
    else
    {
        Console.WriteLine($"エラー: {ex.Message}");
    }
}

単一タスクを await する場合、通常は例外は AggregateException にラップされません。ただし WhenAll のような場合は複数例外を扱う必要があります!

5. 非同期デリゲートとエラーハンドリング

WPF、WinForms、ASP.NET のような UI アプリケーションでは、イベントハンドラを非同期ラムダで書くことがよくあります。もしハンドラ内の例外が外に出ると、UI フレームワーク次第でアプリがクラッシュしたり、エラーが握りつぶされたりします。

推奨

非同期デリゲートの内部では常に try-catch を使いましょう:

button.Click += async (sender, args) =>
{
    try
    {
        await SendReportAsync();
    }
    catch (Exception ex)
    {
        MessageBox.Show($"エラー: {ex.Message}");
    }
};
2
タスク
C# SELF, レベル 59, レッスン 4
ロック未解除
ハンドラー内での非同期例外処理
ハンドラー内での非同期例外処理
1
アンケート/クイズ
アシンクロナスプログラミング、レベル 59、レッスン 4
使用不可
アシンクロナスプログラミング
アシンクロナス vs. マルチスレッド
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION