22

I am working on a Task parallel problem that I have many Tasks that may or may not throw Exception.

I want to process all the tasks that finishes properly and log the rest. The Task.WhenAll propage the Task exception without allowing me to gather the rest results.

    static readonly Task<string> NormalTask1 = Task.FromResult("Task result 1");
    static readonly Task<string> NormalTask2 = Task.FromResult("Task result 2");
    static readonly Task<string> ExceptionTk = Task.FromException<string>(new Exception("Bad Task"));
    var results = await Task.WhenAll(new []{ NormalTask1,NormalTask2,ExceptionTk});

The Task.WhenAll with throw the Exception of ExcceptionTk ignoring the rest results. How I can get the results ignoring the Exception and log the exception at same time?

I could wrap the task into another task that try{...}catch(){...} the internal exception but I don't have access to them and I hope I will not have to add this overhead.

Menelaos Vergis
  • 3,715
  • 5
  • 30
  • 46
  • You can check each individual task's status. You'll find that NormalTask1 and NormalTask2 are RanToCompletion and ExceptionTk is Faulted. – Dennis_E Sep 20 '16 at 09:17
  • There is no overhead involved in *catching* an exception. If exceptions are so frequent that you want to ignore them, you could use wrapper functions that convert all results to a `Result` type and treat them uniformly. That's part of the Railroad-oriented programming style – Panagiotis Kanavos Sep 20 '16 at 09:18
  • @PanagiotisKanavos The overhead was meant for the Task wrapping, not for catching the exception. – Menelaos Vergis Sep 20 '16 at 12:13
  • 1
    Ooops, didn't notice you were calling `WaitAll` instead of `WhenAll`. Anyway, with `WaitAll` you have an expensive blocking operation already. Wrapping the response in a result class is a better option in any case. I suspect you have a pipeline of processing tasks, in which case you should also look at TPL Dataflow – Panagiotis Kanavos Sep 20 '16 at 13:45

3 Answers3

25

You can create a method like this to use instead of Task.WhenAll:

public Task<ResultOrException<T>[]> WhenAllOrException<T>(IEnumerable<Task<T>> tasks)
{    
    return Task.WhenAll(
        tasks.Select(
            task => task.ContinueWith(
                t => t.IsFaulted
                    ? new ResultOrException<T>(t.Exception)
                    : new ResultOrException<T>(t.Result))));
}


public class ResultOrException<T>
{
    public ResultOrException(T result)
    {
        IsSuccess = true;
        Result = result;
    }

    public ResultOrException(Exception ex)
    {
        IsSuccess = false;
        Exception = ex;
    }

    public bool IsSuccess { get; }
    public T Result { get; }
    public Exception Exception { get; }
}

Then you can check each result to see if it was successful or not.


EDIT: the code above doesn't handle cancellation; here's an alternative implementation:

public Task<ResultOrException<T>[]> WhenAllOrException<T>(IEnumerable<Task<T>> tasks)
{    
    return Task.WhenAll(tasks.Select(task => WrapResultOrException(task)));
}

private async Task<ResultOrException<T>> WrapResultOrException<T>(Task<T> task)
{
    try
    {           
        var result = await task;
        return new ResultOrException<T>(result);
    }
    catch (Exception ex)
    {
        return new ResultOrException<T>(ex);
    }
}
Thomas Levesque
  • 286,951
  • 70
  • 623
  • 758
  • 1
    That's almost like Railroad oriented programming. Perhaps the OP should consider changing the original functions themselves to return a `Result` value, or use a wrapper function to convert all Task outcomes to `Result<>` – Panagiotis Kanavos Sep 20 '16 at 09:16
  • What if a task was cancelled? If you want to handle that case too, replace `t.IsFaulted` with `t.Exception != null`. – svick Sep 20 '16 at 09:17
  • @svick good point. Checking `t.Exception` wouldn't work, as a cancelled task might not have an exception set on it (just checked with `Task.FromCanceled`). See my updated answer. – Thomas Levesque Sep 20 '16 at 09:28
  • A possible improvement would be to use TaskContinuationOptions to define separate continuations for faulted, cancelled, completed continuations. This would make it easier to handle separate cases and would get rid of the conditional – Panagiotis Kanavos Sep 20 '16 at 10:27
  • I used the second part of your answer to handle the errors, thank you. – Menelaos Vergis Sep 20 '16 at 13:10
  • A `Task` already has properties `Result` and `Exception`, which calls into question the need for a class like the `ResultOrException`. – Theodor Zoulias Apr 03 '20 at 18:15
6

You can get the result of each successfully completed Task<TResult> from its property Result.

Task<string> normalTask1 = Task.FromResult("Task result 1");
Task<string> normalTask2 = Task.FromResult("Task result 2");
Task<string> exceptionTk = Task.FromException<string>(new Exception("Bad Task"));

Task<string>[] tasks = new[] { normalTask1, normalTask2, exceptionTk };
Task whenAll = Task.WhenAll(tasks);
try
{
    await whenAll;
}
catch
{
    if (whenAll.IsFaulted) // There is also the possibility of being canceled
    {
        foreach (Exception ex in whenAll.Exception.InnerExceptions)
        {
            Console.WriteLine(ex); // Log each exception
        }
    }
}

string[] results = tasks
    .Where(t => t.IsCompletedSuccessfully)
    .Select(t => t.Result)
    .ToArray();
Console.WriteLine($"Results: {String.Join(", ", results)}");

Output:

System.Exception: Bad Task  
Results: Task result 1, Task result 2  
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • Thank you Thodore, I haven't tested your code but I suppose that when the exception is thrown the `WaitAll` exits and `catch` handles the logging. By that time the other tasks might not have returned and when i try to gather the results everything seems running or cancelled. Try adding some load on the `NormalTasks` (`Thread.Sleep`). – Menelaos Vergis Apr 03 '20 at 19:13
  • Hi @MenelaosVergis! The `Task.WaitAll` waits until all tasks have been completed, either successfully or unsuccessfully (with status faulted or canceled). Maybe you confuse it with [`Task.WaitAny`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.waitany)? – Theodor Zoulias Apr 03 '20 at 19:46
  • You are right that I confuse it with something, not with `Task.WaitAny` but with `Task.WhenAll`. – Menelaos Vergis Apr 04 '20 at 07:41
  • @MenelaosVergis you could take a look at this question: [WaitAll vs WhenAll](https://stackoverflow.com/questions/6123406/waitall-vs-whenall). Commonly the `Task.WhenAll` is used differently from `Task.WaitAll`. If you just `await Task.WhenAll` inside a try-catch block, you'll observe only one exception. This behavior is caused by the `await` operator. But it is possible to observe all the exceptions. Just store the task returned by `Task.WhenAll` in a variable, and then examine its `Exception` property inside the catch block. – Theodor Zoulias Apr 04 '20 at 09:44
  • Yes, I have experienced this at my own and this is why i have created this question. Read the question again, i have updated the typo from `WaitAll` to `WhenAll`. – Menelaos Vergis Apr 04 '20 at 12:14
  • @MenelaosVergis yeap, I noticed your update after I posted the previous comment. I have updated my answer too. My updated answer assumes that you are not interested for the case that some tasks have been canceled, or for associating each exception with its originated task. If you want to collect maximum information, then your best bet is to enumerate the tasks and investigate the [`Exception`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.exception) property of each one. – Theodor Zoulias Apr 04 '20 at 13:31
  • Much simpler, great job! – Menelaos Vergis Apr 04 '20 at 16:56
0

You can add HOC with exception handling and then check success.

 class Program
{
    static async Task Main(string[] args)
    {
        var itemsToProcess = new[] { "one", "two" };
        var results = itemsToProcess.ToDictionary(x => x, async (item) =>
        {
            try
            {
                var result = await DoAsync();
                return ((Exception)null, result);
            }
            catch (Exception ex)
            {
                return (ex, (object)null);
            }
        });

        await Task.WhenAll(results.Values);

        foreach(var item in results)
        {
            Console.WriteLine(item.Key + (await item.Value).Item1 != null ? " Failed" : "Succeed");
        }
    }

    public static async Task<object> DoAsync()
    {
        await Task.Delay(10);
        throw new InvalidOperationException();
    }
}
Support Ukraine
  • 978
  • 6
  • 20