When a task fails we cannot access its Result
property because it throws. So to have the results of a partially successful WhenAll
task, we must ensure that the task will complete successfully. The problem then becomes what to do with the exceptions of the failed internal tasks. Swallowing them is probably not a good idea. At least we would like to log them. Here is an implementation of an alternative WhenAll
that never throws, but returns both the results and the exceptions in a ValueTuple
struct.
public static Task<(T[] Results, Exception[] Exceptions)> WhenAllEx<T>(
params Task<T>[] tasks)
{
ArgumentNullException.ThrowIfNull(tasks);
tasks = tasks.ToArray(); // Defensive copy
return Task.WhenAll(tasks).ContinueWith(t => // return a continuation of WhenAll
{
T[] results = tasks
.Where(t => t.IsCompletedSuccessfully)
.Select(t => t.Result)
.ToArray();
AggregateException[] aggregateExceptions = tasks
.Where(t => t.IsFaulted)
.Select(t => t.Exception) // The Exception is of type AggregateException
.ToArray();
Exception[] exceptions = new AggregateException(aggregateExceptions).Flatten()
.InnerExceptions.ToArray(); // Flatten the hierarchy of AggregateExceptions
if (exceptions.Length == 0 && t.IsCanceled)
{
// No exceptions and at least one task was canceled
exceptions = new[] { new TaskCanceledException(t) };
}
return (results, exceptions);
}, default, TaskContinuationOptions.DenyChildAttach |
TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
}
Usage example:
var tPass1 = Task.FromResult(1);
var tFail1 = Task.FromException<int>(new ArgumentException("fail1"));
var tFail2 = Task.FromException<int>(new ArgumentException("fail2"));
var task = WhenAllEx(tPass1, tFail1, tFail2);
task.Wait();
Console.WriteLine($"Status: {task.Status}");
Console.WriteLine($"Results: {String.Join(", ", task.Result.Results)}");
Console.WriteLine($"Exceptions: {String.Join(", ", task.Result.Exceptions.Select(ex => ex.Message))}");
Output:
Status: RanToCompletion
Results: 1
Exceptions: fail1, fail2