8

Given the following:

var tPass1 = Task.FromResult(1);
var tFail1 = Task.FromException<int>(new ArgumentException("fail1"));
var tFail2 = Task.FromException<int>(new ArgumentException("fail2"));

var task = Task.WhenAll(tPass1, tFail1, tFail2);
task.Wait();

the call to task.Wait() throws an AggregateException, whose inner exceptions contain the fail1 and fail2 exceptions. But how can I access the tPass1 successful result?

Is this possible?

I'm aware that I can get the result from the individual task after the WhenAll has finished, via tPass1.Result however is there a way to get them in an array to avoid having to manually track all the things feeding into the WhenAll?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Orion Edwards
  • 121,657
  • 64
  • 239
  • 328
  • 2
    You are passing array of tasks to `Task.WhenAll`, can you use same array to observe results? – Fabio Apr 28 '19 at 04:31
  • https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.status?view=netframework-4.8 – mjwills Apr 28 '19 at 04:34
  • You will of course need a List or an array to track the results. There is no fairy to do it for you. Not clear if that's what you're asking. – H H Apr 28 '19 at 08:02
  • @HenkHolterman basically yes I'm asking for a fairy to track the results for me; Task.WhenAll is already tracking the results for the case where there are no exceptions, and it's already tracking the exceptions. I was hoping that there was simply some way to expose that existing information – Orion Edwards Apr 28 '19 at 09:47

5 Answers5

11

Maybe

public async Task<Task[]> RejectFailedFrom(params Task[] tasks)
{
    try
    {
        await Task.WhenAll(tasks);
    }
    catch(Exception exception)
    {
        // Handle failed tasks maybe
    }

    return tasks.Where(task => task.Status == TaskStatus.RanToCompletion).ToArray();
}

Usage

var tasks = new[]
{
    Task.FromResult(1),
    Task.FromException<int>(new ArgumentException("fail1")),
    Task.FromException<int>(new ArgumentException("fail2"))
};

var succeed = await RejectFailedFrom(tasks);
// [ tasks[0] ]
Fabio
  • 31,528
  • 4
  • 33
  • 72
9

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
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • Good answer. `ContinueWith` is always a great help with `Task`s. – CSDev Apr 28 '19 at 16:11
  • 3
    @Alex, `async-await` leads to much more cleaner and readable code, and have much lighter implementation then continuation, where you should remember to use `ExecuteSynchronously` options. – Basin Apr 29 '19 at 05:51
  • @Basin this is true. With `await` in library code you must remember to use `ConfigureAwait(false)` though. – Theodor Zoulias Apr 29 '19 at 09:37
  • @TheodorZoulias ConfigureAwait(false) case depends on scenario, it is not a must. Suppose if capturing the context is actually required within library itself. – Sarvesh Mishra Apr 29 '19 at 11:13
  • @SarveshMishra this is even worse then. You must ConfigureAwait(true/false) case by case. Easy to make an error. The more awaits, the more possibilities for error. – Theodor Zoulias Apr 29 '19 at 11:25
  • @TheodorZoulias, in .NET Core you don't need to use `ConfigureAwait`. – Fabio May 01 '19 at 10:01
  • @Fabio [here](https://stackoverflow.com/a/45638071/11178549) it says that for library .NET Core code it is recommended to use `ConfigureAwait(false)`. – Theodor Zoulias May 01 '19 at 17:17
  • @TheodorZoulias, there it says as well when you don't need it, which for .NET Core libraries is most of the time. – Fabio May 01 '19 at 19:48
1

Playing around with @Theodor Zoulias's powerfull and elegant solution pushed me to something. It looks hacky, but still works. One can continue Task.WhenAll with something that will not throw an exception for sure (e.g. _ => { }) and Wait that something.

var cts = new CancellationTokenSource();
cts.Cancel();
var canceled = Task.Run(() => 1, cts.Token);

var faulted = Task.FromException<int>(new Exception("Some Exception"));

var ranToCompletion = Task.FromResult(1);

var allTasks = new[] { canceled, faulted, ranToCompletion };

// wait all tasks to complete regardless anything
Task.WhenAll(allTasks).ContinueWith(_ => { }).Wait();

foreach(var t in allTasks)
{
    Console.WriteLine($"Task #{t.Id} {t.Status}");
    if (t.Status == TaskStatus.Faulted)
        foreach (var e in t.Exception.InnerExceptions)
            Console.WriteLine($"\t{e.Message}");
    if (t.Status == TaskStatus.RanToCompletion)
        Console.WriteLine($"\tResult: {t.Result}");
}

Output looks like this:

Task #2 Canceled
Task #1 Faulted
        Some Exception
Task #5 RanToCompletion
        Result: 1
CSDev
  • 3,177
  • 6
  • 19
  • 37
0
ConcurrentQueue<Exception> errorList = new();

var someTasks = makeSomeActionAsync().ContinueWith(x => 
{
   if(x.Exception !=null)
      errorList.Enqueue(x.Exception);
});

await Task.WhenAll(someTasks);

if(errorList.Any())
   throw new Exception($"\n{string.Join("\n", errorList )}");
Dmytro
  • 1
  • 3
    Please provide additional details in your answer. As it's currently written, it's hard to understand your solution. – Community Sep 02 '21 at 08:33
-2

Change

var task = Task.WhenAll(tPass1, tFail1, tFail2);
task.Wait();

to

var all = new Task<int>[] { tPass1, tFail1, tFail2 }
    .Where(t => t.Status == TaskStatus.RanToCompletion);
var task = Task.WhenAll(all);
task.Wait();

Working example

Ted Mucuzany
  • 33
  • 1
  • 3
  • 8