24

I have two tasks. I run both of them with Task.WhenAll. What happens if one of them throws an exception? Would the other one complete?

Jonatan Dragon
  • 4,675
  • 3
  • 30
  • 38
  • 1
    If you are interested to a `WhenAll` implementation that fails immediately on first exception, [here](https://stackoverflow.com/questions/57313252/how-can-i-await-an-array-of-tasks-and-stop-waiting-on-first-exception) is a relevant question. – Theodor Zoulias Apr 17 '20 at 19:18

3 Answers3

12

Just run this code to test it:

private static async Task TestTaskWhenAll()
{
    try
    {
        await Task.WhenAll(
            ShortOperationAsync(),
            LongOperationAsync()
        );
    }
    catch (Exception exception)
    {
        Console.WriteLine(exception.Message); // Short operation exception
        Debugger.Break();
    }
}

private static async Task ShortOperationAsync()
{
    await Task.Delay(1000);
    throw new InvalidTimeZoneException("Short operation exception");

}

private static async Task LongOperationAsync()
{
    await Task.Delay(5000);
    throw new ArgumentException("Long operation exception");
}

Debugger will stop in 5 seconds. Both exceptions are thrown, but Debugger.Break() is hit only once. What is more, the exception value is not AggregateException, but InvalidTimeZoneException. This is because of new async/await which does the unwrapping into the actual exception. You can read more here. If you want to read other Exceptions (not only the first one), you would have to read them from the Task returned from WhenAll method call.

Jonatan Dragon
  • 4,675
  • 3
  • 30
  • 38
11

Will the other one complete?

It won't be stopped as a result of the other one failing.

But will it complete?

Task.When will wait for all to complete, whether any or none fail. I just tested with this to verify - and it took 5 seconds to complete:

Task allTasks = Task.WhenAll(getClientToken, getVault, Task.Delay(5000)); 

If you want to group the tasks you can create a 'new task', then await that.

Task allTasks = Task.WhenAll(getClientToken, getVault, Task.Delay(5000)); 

try 
{
    await allTasks;

} catch (Exception ex) 
{

   // ex is the 'unwrapped' actual exception
   // I'm not actually sure if it's the first task to fail, or the first in the list that failed

   // Handle all if needed
   Exceptions[] allExceptions = allTasks.Exceptions;

   // OR
   // just get the result from the task / exception
   if (getVault.Status == TaskStatus.Faulted) 
   {
       ...
   }
}
Community
  • 1
  • 1
Simon_Weaver
  • 140,023
  • 84
  • 646
  • 689
4

Had the same question and tested by myself. In short:

  • It always wait for all tasks to finish.

  • The first exception is thrown if there is any after all tasks are finished (crash if you don't catch).

  • For all exceptions, keep the Task instance returned by Task.WhenAll and use Exception.InnerExceptions property.

Here's my test:

    static async Task Main(string[] args)
    {
        var tasks = new[] { Foo1(), Foo2(), Foo3() };

        Task t = null;
        try
        {
            t = Task.WhenAll(tasks);
            await t;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
        }

        Console.WriteLine("All have run.");

        if (t.Exception != null) 
        {
            foreach (var ex in t.Exception.InnerExceptions)
            {
                Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
            }
        }

    }

    static async Task Foo1()
    {
        await Task.Delay(50);
        throw new ArgumentException("zzz");
    }

    static async Task Foo2()
    {
        await Task.Delay(1000);
        Console.WriteLine("Foo 2");
        throw new FieldAccessException("xxx");
    }

    static async Task Foo3()
    {
        for (int i = 0; i < 10; i++)
        {
            await Task.Delay(200);
            Console.WriteLine("Foo 3");
        }
    }

Output:

Foo 3
Foo 3
Foo 3
Foo 3
Foo 2
Foo 3
Foo 3
Foo 3
Foo 3
Foo 3
Foo 3
ArgumentException: zzz
All have run.
ArgumentException: zzz
FieldAccessException: xxx
Luke Vo
  • 17,859
  • 21
  • 105
  • 181
  • 1
    Initializing the task with `Task t = null;` is error prone. `Task t;` is safer because it forces you to initialize the variable inside the try block, otherwise your code won't compile. Also be aware that `t.Exception` will be `null` in most cases (because hopefully the task will be completed successfully most of the time). So your current code will cause a `NullReferenceException` in the success case. – Theodor Zoulias Apr 17 '20 at 19:14
  • In my case it wouldn't compile if I use `Task t;` though since the assignment of `t` is inside the `try` block. And yeah, I should have added a null check for `t.Exception`, thanks! – Luke Vo Apr 17 '20 at 19:21
  • You are right, my previous suggestion isn't compiling. How about initializing the variable before entering the `try` block? `Task t = Task.WhenAll(tasks);` Otherwise you should also check for the case that `t == null` to be 100% safe, which is easy to forget. – Theodor Zoulias Apr 17 '20 at 19:29
  • the observation is good but this sample is not entirely correct; construct the tasks in a manner that when they are iterated on, they are run/scheduled -- i.e. LazyLoading with `IEnumerable`. Your code is current running/scheduling 3 tasks by invocation when defining that array of tasks then -- using the `Task.WhenAll` -- you're ensure those running tasks are completed in execution flow. Instead, have `Task.WhenAll` perform the execution of runnable tasks; `var runnableTasks = new [] { Foo1, Foo2, Foo3}.Select(async (fn) => await fn())` then `var t = Task.WhenAll(runnableTasks);` – Brett Caswell Mar 15 '22 at 07:47
  • if you added `await Task.Delay(10000);` just after `var tasks = new[] { Foo1(), Foo2(), Foo3() };` I believe you'll observe they are running. – Brett Caswell Mar 15 '22 at 07:49
  • screenshot: https://imgur.com/a/3a1ps0L of that tasks running; Having said that, your observation is still valid, but the behavior on whether or not `Task.WhenAll` can **ever** handle faulting tasks (without ContinueWith) may depend on whether the IEnumerable tasks are 'loaded' or running/awaiting/scheduled prior it's usage. also, of course, running of `Foo1()` via invocation could cause an exception to raise outside of your exception handling block (if you `await Task.Delay` prior to try-catch) in your sample. – Brett Caswell Mar 15 '22 at 08:04