LoginSignup
46
52

More than 1 year has passed since last update.

[C#] Taskの中で例外が起きた時のキャッチの仕方

Last updated at Posted at 2021-02-04

もくじ

やりたいこと

Taskで非同期処理をしているときに、Taskを.Wait()する処理があった。

そこで例外が発生したときに、そいつをtry catchすると、起きていたはずの例外ではなくAggregateExceptionという例外がスローされていた。

元々タスクの中では別の例外が起きていたはずなのに、そいつはどこに行ったのか?
実際に起きた例外をキャッチするにはどうしたらよいのか?調べたい。

やったこと

いろいろ調べたところ、
どうやら待ち方によって例外の受け取り方を変えないといけない様子。
今回調べた限りでは、下記のような待ち方ごとの例外のキャッチの仕方があった。

①awaitで待つときのパターン

awaitするときは、そこをtry catchで囲むだけで、通常通りに例外がcatchできる。

// awaitしたTaskの例外
private async  void Button_Click(object sender, RoutedEventArgs e)
{
    try
    {
        await Task.Run(() =>
        {
            throw new NotImplementedException();
        });
    }
    catch (Exception ex)
    {
        Debug.WriteLine("1:" + ex.GetType());
    }
}

②Wait()で待つときのパターン

Wait()したときは、例外がAggregateExceptionに包まれて上がってくる。
実際に何の例外が起きたかは、AggregateExceptionInnerExceptionプロパティを見る必要がある。

// Wait()したTaskの例外
private void Button_Click_1(object sender, RoutedEventArgs e)
{
    try
    {
        Task.Run(() =>
        {
            throw new NotImplementedException();
        }).Wait();
    }
    catch (Exception ex)
    {
        Debug.WriteLine("a:" + ex.GetType());
        if (ex is AggregateException age)
        {
            Debug.WriteLine("b:" + age.InnerException.GetType());
        }
    }
}

③待たないパターン

待たないタスクの場合は、普通にtry catchで囲んでも例外は取れない。
(その場で処理してないのだから当たり前かも)

その場合は、終わったタスクの変数のExceptionプロパティを見て、何の例外が起きてタスクが終わったのかを調べる。

実験では、例外を調べたいタスクのContinueWith()メソッドを使って、タスクが終わった時に行う処理を登録して、その中でExceptionプロパティを見た。

またその時も、Wait()のときと同じようにAggregateExceptionに包まれて上がってきてるので、実際に何の例外が起きたかはAggregateExceptionInnerExceptionプロパティを見る。

// 待たないTaskの例外
private void Button_Click_2(object sender, RoutedEventArgs e)
{
    try
    {
        var t = Task.Run(() =>
        {
            throw new NotImplementedException();
        });

        t.ContinueWith((compt) =>
        {
            Debug.WriteLine("A:" + compt.Exception.GetType());
            if (compt.Exception is AggregateException age)
            {
                Debug.WriteLine("C:" + age.InnerException.GetType());
            }
        });
    }
    catch (Exception ex)
    {
        // ここには来ない
        Debug.WriteLine("B:" + ex.GetType());
    }
}

[番外編] 複数TaskをTask.WhenAll()で待った時に各タスクの中で起きた例外をまとめて取る

複数のタスクを**Task.WhenAll()**で待ったときに、それぞれのタスクで例外が起きていた時にそれを纏めて取ることができる。
ただ直感的には取れず、少々小細工必要。

**Task.WhenAll()**をtry catchでキャッチした例外は、複数例外がまとめられたAggregateExceptionではなく、各タスクで起きた例外のうちの1つだけが入ったものになっている。

起きた例外全部を拾おうとすると、**Task.WhenAll()**のタスクを受けたローカル変数の中のExceptionプロパティを見る必要がある。(それがAggregateExceptionになっている)

private async void Button_Click_4(object sender, RoutedEventArgs e)
{
    var t1 = Task.Run(() => { throw new NotImplementedException(); });
    var t2 = Task.Run(() => { throw new ArgumentException(); });
    var t3 = Task.Run(() => { throw new InvalidOperationException(); });
    var all = Task.WhenAll(t1, t2, t3);

    try
    {
        // WhenAllのタスクのローカル変数を作って、それをtry catchする
        await all;
    }
    catch (Exception ex)
    {
        // ex には例外のうちの1つしか入ってないので、
        // WhenAllのタスクのローカル変数のExceptionプロパティ
        // (それがAggregateExceptionになってる)を見て
        // すべての例外を取り出す
        if (all.Exception is AggregateException age)
        {
            age.InnerExceptions.ToList().ForEach((ages) => Debug.WriteLine(ages.GetType()));
        }
    }
}

上記のage.InnerExceptions.ToList().ForEach((ages) => Debug.WriteLine(ages.GetType()));の部分では、例外AggregateExceptionを完全に握りつぶしている(再throwしない)が、AggregateExceptionHandleメソッドを使っても似たようなことが書ける。

その場合、Handleでtrueを返すようにすると、例外はそこで処理済みとして、再throwしないようにもできる。(falseだと再throwする)

private async void Button_Click_4(object sender, RoutedEventArgs e)
{
    var t1 = Task.Run(() => { throw new NotImplementedException(); });
    var t2 = Task.Run(() => { throw new ArgumentException(); });
    var t3 = Task.Run(() => { throw new InvalidOperationException(); });
    var all = Task.WhenAll(t1, t2, t3);

    try
    {
        await all;
    }
    catch (Exception ex)
    {
        if (all.Exception is AggregateException age)
        {
            age.Handle((excep) =>
            {
                // excep はAggregateExceptionに包まれている個別の例外。
                Debug.WriteLine(excep.GetType());

                // trueにしたら、ここでもう処理済みということで例外を再throwしない
                // falseにしたら、まだ未処理ということで例外を再throwする。
                return true;
            });
        }
    }
}

AggregateExceptionを均すやり方

AggregateExceptionは、その中にさらにAggregateExceptionが入ることもあるので、それを一気に取り出すようなやり方がある様子。

foreach (var inner in exception.Flatten().InnerExceptions)
{
    Console.WriteLine(inner.Message);
    Console.WriteLine("Type : {0}", inner.GetType());
}

戻り値のあるタスクの完了をTask.WhenAll()で待って例外が起きたときに、正常終了したTaskだけでも値を取り出す

下記に試したこと/やり方をまとめた。

参考

■MS公式 Async・Await 非同期プログラミングのベスト プラクティス
Async・Awaitの使い方から例外の処理の仕方まで、細かく書いてくれてる。
@SSSSYYYY さんのコメントでこのページを知りました。ありがとうございます!
https://docs.microsoft.com/ja-jp/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming

■async/awaitで例外処理をするには?
https://www.atmarkit.co.jp/ait/articles/1805/16/news018.html

■【C# Tips】非同期処理(Task)の例外処理を極めて、障害を正しく検知しよう!!
https://www.sukerou.com/2018/09/task.html

■async/awaitによる非同期処理の例外の謎
いろいろ試行錯誤されてた様子。なにげにデバッグ実行したときに、一旦例外スロー部分でとまるけど続けてF5おしたら動かせる、という部分がへぇーとなった。
https://qiita.com/habu1010/items/08177698fa3826474c0b

■MS公式 AggregateException
https://docs.microsoft.com/ja-jp/dotnet/api/system.aggregateexception?view=net-5.0
※公式サンプルでは、AggregateException の Handleメソッドを使ってAggregateException の中の複数の例外を処理しているみたい。

46
52
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
46
52