C#で複数のExceptionがまとめられたAggregateExceptionを正しくハンドリングする方法についてまとめました。
AggregateExceptionとは?
たとえばTask.WhenAll
で複数のTaskの完了を待機するときには、複数の例外がスローされる可能性があります。
WhenAll
をawaitするとスローされた例外のうちのいずれかだけしかキャッチできませんが、以下のようにAggregateException型のTask.Exceptionを参照することで、全ての例外の情報を取得することができます。
public sealed class Tests { [Test] public async Task Test() { var whenAllTask = Task.WhenAll( Task.FromException(new Exception("Foo Exception")), Task.FromException(new Exception("Bar Exception")) ); try { await whenAllTask; } catch { // テストのため例外を握りつぶす } // 発生した例外がwhenAllTask.Exception(AggregateException型)にまとめられていることを確認 Assert.That(whenAllTask.Exception, Is.Not.Null); // InnnerExceptionsで個々の例外を取得できる Assert.That(whenAllTask.Exception!.InnerExceptions.Count, Is.EqualTo(2)); Assert.That(whenAllTask.Exception!.InnerExceptions.Any(e => e.Message == "Foo Exception")); Assert.That(whenAllTask.Exception!.InnerExceptions.Any(e => e.Message == "Bar Exception")); } }
Flatten
またより複雑な例として、以下のページに載っているような親子関係のあるTaskを作る場合には、AggregateException自体が入れ子になる可能性もあります。
このようなケースでは以下のように、AggregateException.Flatten()
を呼ぶことで複数のAggregateException
を一つにまとめることができます。
public sealed class Tests { [Test] public async Task Test() { var task = Task.Factory.StartNew(() => { Task.Factory.StartNew(() => { Task.Factory.StartNew(() => throw new Exception("Attached child2 faulted."), TaskCreationOptions.AttachedToParent); throw new Exception("Attached child1 faulted."); }, TaskCreationOptions.AttachedToParent); }); try { await task; } catch { // テストのため例外を握りつぶす } // AggregateExceptionが入れ子になっていることを確認 Assert.That(task.Exception, Is.TypeOf<AggregateException>()); Assert.That(task.Exception!.InnerException, Is.TypeOf<AggregateException>()); Assert.That(task.Exception!.InnerException!.InnerException, Is.TypeOf<Exception>()); // Flattenして全ての例外が取得できることを確認 var exceptions = task.Exception!.Flatten().InnerExceptions; Assert.That(exceptions.Count, Is.EqualTo(2)); Assert.That(exceptions.Any(e => e.Message == "Attached child1 faulted.")); Assert.That(exceptions.Any(e => e.Message == "Attached child2 faulted.")); } }
Handle
さらに、Catch句内でAggregateException.Handle
を呼ぶと特定の例外のみ処理済みとしてマークすることができます。
未処理のままにした例外については、AggregateException
のInnerExceptions
に設定されて再びスローされます。
public sealed class Tests { [Test] public async Task Test() { var task = Task.Factory.StartNew(() => { Task.Factory.StartNew(() => { Task.Factory.StartNew(() => throw new Exception("Attached child2 faulted."), TaskCreationOptions.AttachedToParent); throw new Exception("Attached child1 faulted."); }, TaskCreationOptions.AttachedToParent); }); AggregateException? exception = null; try { try { await task; } catch (AggregateException ae) { ae.Flatten() .Handle(ex => { // 特定の例外だけ処理済みとする if (ex.Message == "Attached child1 faulted.") return true; // 他は未処理例外としてAggregateExceptionのInnerExceptionとして再スローされる return false; }); } } catch (AggregateException ae) { // Handleされた後のAggregateException exception = ae; } // AggregateExceptionのInnerExceptionにはHandleされなかった例外が含まれることを確認 Assert.That(exception, Is.TypeOf<AggregateException>()); Assert.That(exception!.InnerExceptions.Count, Is.EqualTo(1)); Assert.That(exception!.InnerException!.Message == "Attached child2 faulted."); } }