Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to get successful results from a Task.WhenAll when one of the tasks fails? [duplicate]

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?

like image 592
Orion Edwards Avatar asked Dec 08 '22 12:12

Orion Edwards


2 Answers

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] ]
like image 82
Fabio Avatar answered Dec 10 '22 03:12

Fabio


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
like image 41
Theodor Zoulias Avatar answered Dec 10 '22 03:12

Theodor Zoulias