C#のTask.WhenAnyで例外を正しくハンドリングする方法です。
WhenAnyは例外をスローしない
.NETでは、複数のTaskを待機する方法として、全てのTaskの完了を待機するTask.WhenAll
といずれかのTaskの完了を待機するTask.WhenAny
があります。
このうちTask.WhenAll
は、待機するTaskのうちのいずれかが例外をスローしたときに、このWhenAll
が返すTaskも例外をスローします。
public sealed class Tests { [Test] public async Task WhenAllTest() { var foo = FooAsync(); // 例外を吐くTask var bar = BarAsync(); // WhenAllで両方が終わるまで待機 var task = Task.WhenAll(foo, bar); Exception exception = null; try { await task; } catch (Exception e) { exception = e; } // 例外がスローされたことを確認 Assert.That(exception, Is.Not.Null); } private async Task FooAsync() { await Task.Run(() => throw new Exception("Foo Exception")); } private async Task BarAsync() { await Task.Delay(100); } }
(実際にはここでスローされる例外は「元となるTaskがスローした例外たちのうちのいずれか」になるので、ちゃんとハンドリングするにはTask.Exception (AggregateException)
を見る必要がありますがそのあたりは割愛します
さてこれに対してTask.WhenAny
は、元となるTaskが例外をスローしてもWhenAny
のTaskは例外をスローしません。
public sealed class Tests { [Test] public async Task WhenAnyTest() { var foo = FooAsync(); // 例外を吐くTask var bar = BarAsync(); // WhenAnyでどちらかが終わるまで待機 // 例外を吐くFooAsyncの方が先に終わる実装にしてある var task = Task.WhenAny(foo, bar); Exception exception = null; try { await task; } catch (Exception e) { exception = e; } // 例外がスローされていないことを確認 Assert.That(exception, Is.Null); } private async Task FooAsync() { await Task.Run(() => throw new Exception("Foo Exception")); } private async Task BarAsync() { await Task.Delay(100); } }
このようにTask.WhenAnyは元となるTaskが例外をスローしていたとしても例外を吐かずに常に正常に完了状態になります。
WhenAnyで正しく例外をハンドリングする
WhenAny
をawaitすると、最初に完了したTaskを得ることができます。
このTaskを以下のようにawait(つまり2回await)すると、例外が発生している場合には例外がスローされるので、ハンドリングすることができます。
public sealed class Tests { [Test] public async Task WhenAnyTest() { var foo = FooAsync(); // 例外を吐くTask var bar = BarAsync(); // WhenAnyをawaitすると最初に完了したTask(今回はfoo)を取得できる var completedTask = await Task.WhenAny(foo, bar); // 最初に完了したTaskをawaitすると例外をスローすることを確認 Exception? exception = null; try { await completedTask; } catch (Exception e) { exception = e; } Assert.That(exception, Is.Not.Null); Assert.That(exception?.Message, Is.EqualTo("Foo Exception")); } private async Task FooAsync() { await Task.Run(() => throw new Exception("Foo Exception")); } private async Task BarAsync() { await Task.Delay(100); } }
またもちろん以下のように、各Taskの状態を見てハンドリングすることも可能です。
public sealed class Tests { [Test] public async Task WhenAnyTest() { var foo = FooAsync(); var bar = BarAsync(); var completedTask = await Task.WhenAny(foo, bar); Assert.Multiple(() => { Assert.That(completedTask, Is.EqualTo(foo)); Assert.That(foo.IsCompleted, Is.True); Assert.That(bar.IsCompleted, Is.False); Assert.That(foo.IsFaulted, Is.True); Assert.That(bar.IsFaulted, Is.False); Assert.That(foo.Exception!.InnerException!.Message, Is.EqualTo("Foo Exception")); }); } private async Task FooAsync() { await Task.Run(() => throw new Exception("Foo Exception")); } private async Task BarAsync() { await Task.Delay(100); } }