1

If I do await Task.WhenAll(tasks) it throws exception if one or more tasks failed. I would like to examine each task objects manually.

Usually it is not good idea to catch(Exception). Is it reasonable to do it in this case? Or maybe there is another way to wait for all tasks? I cannot catch AggregateException, because if there is just one it will be unwrapped.

How to do it correctly in async method?

Shadow
  • 2,089
  • 2
  • 23
  • 45
  • 4
    _"Usually it is not good idea to catch(Exception)"_ -- sure...but that doesn't mean you can't catch _any_ exception. The rules (guidelines, really) for exception-handling don't change just because you're using `await`. You still need to know what exceptions might happen, which you can handle, and catch only those. So, do that. – Peter Duniho Jan 29 '18 at 09:24
  • @Icepickle single exception will be unwrapped, so it will not work in all cases. – Shadow Jan 29 '18 at 10:01
  • @Fabjan I meant "in this case", sorry for putting it incorrectly. – Shadow Jan 29 '18 at 10:02
  • 1
    @PeterDuniho I have no idea what exceptions might happen, because underlying implementation may change. I would like to just take tasks which `RanToCompletion`, adn discard failed ones. – Shadow Jan 29 '18 at 10:09
  • _"I have no idea what exceptions might happen"_ -- well, then you need to change that. You can't go around catching every single exception and then expect to be able to continue safely. Not all exceptions will leave your process in a consistent, reliable state. – Peter Duniho Jan 29 '18 at 17:05
  • Related: [Ignore the Tasks throwing Exceptions at Task.WhenAll and get only the completed results](https://stackoverflow.com/questions/39589640/ignore-the-tasks-throwing-exceptions-at-task-whenall-and-get-only-the-completed) – Theodor Zoulias May 12 '20 at 20:36
  • I'm hijacking this question — I'm building a test runner; it's critical that (1) I catch all exceptions and (2) I see the result of every task, whether that be a value or exception. My answer does this. It also answers @Shadow 's original question. This could also be useful in any case where you want something between `WhenAny` and `WhenAll`, like replicating to `n/m` servers. Further, this is useful any time you want to handle errors with knowledge of the input, which is most of the time... – kelloti May 15 '20 at 17:26

2 Answers2

0

The AwaitCompletion extention method below does what you're looking for, with a more generic API. It returns a Task, so you'll still have to await that, either via await or Task.Wait() or something else.

using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

public static class TaskExtensions
{
    public static Task AwaitCompletion<T>(this ICollection<Task<T>> unfinishedTasks)
    {
        int remainingCount = unfinishedTasks.Count;
        var promise = new TaskCompletionSource<ICollection<Task<T>>>();
        var finishers = unfinishedTasks.Select(x => x.ContinueWith((task, state) =>
        {
            int postCount = Interlocked.Decrement(ref remainingCount);
            if (postCount == 0)
            {
                promise.SetResult(unfinishedTasks);
            }
        }, remainingCount));

        // force evaluation
        var _ = finishers.ToList();

        return promise.Task;
    }

    /// <summary>
    /// Unlike Task.Value, this doesn't wait for completion. Hence, it also doesn't
    /// throw exceptions.
    /// </summary>
    /// <returns>Value if completed, otherwise null (or default value if a value type)</returns>
    public static T GetResultOrDefault<T>(this Task<T> self)
    {
        if (self.IsCompletedSuccessfully)
        {
            return self.Result;
        }
        else
        {
            return default(T);
        }
    }
}

After completion of the returned Task value, all tasks in unfinishedTasks will either be completed or error'd (or still going if you use Task.Wait() with a timeout). AwaitCompletion does not throw exceptions, and awaiting it will only throw exceptions if a timeout is reached. To retrieve exceptions, look at the tasks you passed in.

Usage:

Task[] tasksToWaitFor = ...
await tasksToWaitFor.AwaitCompletion()
foreach (var task in tasksToWaitFor) 
{
    Console.WriteLine("result: {}", task.GetResultOrDefault());
}

The GetResultOrDefault extension method is important. If you hit Value directly, it'll throw AggregateException when the task fails, OTOH GetResultOrDefault will return null.

kelloti
  • 8,705
  • 5
  • 46
  • 82
-2

Swallow the exceptions momentarily with

await Task.WhenAll(tasks).ContinueWith(delegate {} )

but remember to then check all tasks individually and properly surface errors.

Bruno Martinez
  • 2,850
  • 2
  • 39
  • 47
  • The `ContinueWith` is a [primitive](https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html) method, and combining it with `await` is not advisable. – Theodor Zoulias Apr 01 '21 at 21:36
  • @TheodorZoulias what breaks with ContinueWith? I grant you it's ugly. I would love a short alternative. – Bruno Martinez Apr 02 '21 at 01:34
  • The `ContinueWith` doesn't really break anything, especially in this case that the lambda is a no-op. It's just not a good idea to combine two different mechanisms that accomplish the same thing in slightly different ways. The async/await was invented with the intention to replace the `ContinueWith`, not to coexist with it. The preferred way to prevent the propagation of an exception is to `catch` the exception. What the OP wants is probably covered by [this](https://stackoverflow.com/questions/18314961/i-want-await-to-throw-aggregateexception-not-just-the-first-exception) question. – Theodor Zoulias Apr 02 '21 at 01:50
  • I believe async/await is built on top of ContinueWith. I don't see the point on forcing re-throwing the exception to immediately re-catch it, and also getting longer code. – Bruno Martinez Apr 02 '21 at 03:37
  • Why not? Adding a try/catch results to readable and maintainable code, with clear intentions and no surprises. Do you really want to take into account the implications of a possible ambient `TaskScheduler` of unknown origin, that will schedule the execution of the delegate passed to `ContinueWith` in any way it likes? Or you are thrilled with the idea of passing always the `TaskScheduler.Default` as the second argument of the `ContinueWith`, to eliminate this scenario? – Theodor Zoulias Apr 02 '21 at 04:15
  • This isn't about using ContinueWith in general but to avoid throwing the exception. The empty delegate argument is part of the solution. It's even better if it runs in another scheduler and avoid a needless transition. – Bruno Martinez Apr 02 '21 at 13:35
  • Why is `ContinueWith` preferable to a try catch? `try { await Task.WhenAll(tasks); } catch { }`. The try catch is even shorter by 8 characters! But if you really like the idea that your code can be scheduled by an unknown scheduler that is out of your control, then go for it. After all determinism is so boring. Lets allow our code have a life of its own! – Theodor Zoulias Apr 02 '21 at 15:38
  • I just don't see the nondeterminism when running zero code, but I do feel guilty about rising an exception just to catch it in the same line. – Bruno Martinez Apr 03 '21 at 02:57
  • Take a look at this: [Performance cost of a try/catch block](https://stackoverflow.com/questions/3480124/). The cost of catching the exception is minuscule. The exception itself is already raised at this point (an `Exception`-derived object has been allocated in memory). Regarding the non-determinism of zero code, you must take into account the scheduling as well. A custom ambient scheduler could (for example) schedule the execution on a dedicated STA thread, so the zero code would have to sit and wait other arbitrary long-running operations to complete before getting its turn. – Theodor Zoulias Apr 03 '21 at 03:24