Code4IT

The place for .NET enthusiasts, Azure lovers, and backend developers

C# Tip: Handling exceptions with Task.WaitAll and Task.WhenAll

2025-06-10 6 min read CSharp Tips
Just a second! 🫷
If you are here, it means that you are a software developer. So, you know that storage, networking, and domain management have a cost .

If you want to support this blog, please ensure that you have disabled the adblocker for this site. I configured Google AdSense to show as few ADS as possible - I don't want to bother you with lots of ads, but I still need to add some to pay for the resources for my site.

Thank you for your understanding.
- Davide

Asynchronous programming enables you to execute multiple operations without blocking the main thread.

In general, we often think of the Happy Scenario, when all the operations go smoothly, but we rarely consider what to do when an error occurs.

In this article, we will explore how Task.WaitAll and Task.WhenAll behave when an error is thrown in one of the awaited Tasks.

Prepare the tasks to be executed

For the sake of this article, we are going to use a silly method that returns the same number passed in input but throws an exception in case the input number can be divided by 3:

public Task<int> Echo(int value) => Task.Factory.StartNew(
() =>
{
    if (value % 3 == 0)
    {
        Console.WriteLine($"[LOG] You cannot use {value}!");
        throw new Exception($"[EXCEPTION] Value cannot be {value}");
    }
    Console.WriteLine($"[LOG] {value} is a valid value!");
    return value;
}
);

Those Console.WriteLine instructions will allow us to see what’s happening “live”.

We prepare the collection of tasks to be awaited by using a simple Enumerable.Range

var tasks = Enumerable.Range(1, 11).Select(Echo);

And then, we use a try-catch block with some logs to showcase what happens when we run the application.

try
{

    Console.WriteLine("START");

    // await all the tasks

    Console.WriteLine("END");
}
catch (Exception ex)
{
    Console.WriteLine("The exception message is: {0}", ex.Message);
    Console.WriteLine("The exception type is: {0}", ex.GetType().FullName);

    if (ex.InnerException is not null)
    {
        Console.WriteLine("Inner exception: {0}", ex.InnerException.Message);
    }
}
finally
{
    Console.WriteLine("FINALLY!");
}

If we run it all together, we can notice that nothing really happened:

START
END
FINALLY

In fact, we just created a collection of tasks (which does not actually exist, since the result is stored in a lazy-loaded enumeration).

We can, then, call WaitAll and WhenAll to see what happens when an error occurs.

Error handling when using Task.WaitAll

It’s time to execute the tasks stored in the tasks collection, like this:

try
{
    Console.WriteLine("START");

    // await all the tasks
    Task.WaitAll(tasks.ToArray());

    Console.WriteLine("END");
}

Task.WaitAll accepts an array of tasks to be awaited and does not return anything.

The execution goes like this:

START
1 is a valid value!
2 is a valid value!
:(  You cannot use 6!
5 is a valid value!
:(  You cannot use 3!
4 is a valid value!
8 is a valid value!
10 is a valid value!
:(  You cannot use 9!
7 is a valid value!
11 is a valid value!
The exception message is: One or more errors occurred. ([EXCEPTION] Value cannot be 3) ([EXCEPTION] Value cannot be 6) ([EXCEPTION] Value cannot be 9)
The exception type is: System.AggregateException
Inner exception: [EXCEPTION] Value cannot be 3
FINALLY!

There are a few things to notice:

  • the tasks are not executed in sequence: for example, 6 was printed before 4. Well, to be honest, we can say that Console.WriteLine printed the messages in that sequence, but maybe the tasks were executed in another different order (as you can deduce from the order of the error messages);
  • all the tasks are executed before jumping to the catch block;
  • the exception caught in the catch block is of type System.AggregateException; we’ll come back to it later;
  • the InnerException property of the exception being caught contains the info for the first exception that was thrown.

Error handling when using Task.WhenAll

Let’s replace Task.WaitAll with Task.WhenAll.

try
{
    Console.WriteLine("START");

    await Task.WhenAll(tasks);

    Console.WriteLine("END");
}

There are two main differences to notice when comparing Task.WaitAll and Task.WhenAll:

  1. Task.WhenAll accepts in input whatever type of collection (as long as it is an IEnumerable);
  2. it returns a Task that you have to await.

And what happens when we run the program?

START
2 is a valid value!
1 is a valid value!
4 is a valid value!
:(  You cannot use 3!
7 is a valid value!
5 is a valid value!
:(  You cannot use 6!
8 is a valid value!
10 is a valid value!
11 is a valid value!
:(  You cannot use 9!
The exception message is: [EXCEPTION] Value cannot be 3
The exception type is: System.Exception
FINALLY!

Again, there are a few things to notice:

  • just as before, the messages are not printed in order;
  • the exception message contains the message for the first exception thrown;
  • the exception is of type System.Exception, and not System.AggregateException as we saw before.

This means that the first exception breaks everything, and you lose the info about the other exceptions that were thrown.

πŸ“© but now, a question for you: we learned that, when using Task.WhenAll, only the first exception gets caught by the catch block. What happens to the other exceptions? How can we retrieve them? Drop a message in the comment below ⬇️

Comparing Task.WaitAll and Task.WhenAll

Task.WaitAll and Task.WhenAll are similar but not identical.

Task.WaitAll should be used when you are in a synchronous context and need to block the current thread until all tasks are complete. This is common in simple old-style console applications or scenarios where asynchronous programming is not required. However, it is not recommended in UI or modern ASP.NET applications because it can cause deadlocks or freeze the UI.

Task.WhenAll is preferred in modern C# code, especially in asynchronous methods (where you can use async Task). It allows you to await the completion of multiple tasks without blocking the calling thread, making it suitable for environments where responsiveness is important. It also enables easier composition of continuations and better exception handling.

Let’s wrap it up in a table:

Feature Task.WaitAll Task.WhenAll
Return Type void Task or Task<TResult[]>
Blocking/Non-blocking Blocking (waits synchronously) Non-blocking (returns a Task)
Exception Handling Throws AggregateException immediately Exceptions observed when awaited
Usage Context Synchronous code (e.g., console apps) Asynchronous code (e.g., async methods)
Continuation Not possible (since it blocks) Possible (use .ContinueWith or await)
Deadlock Risk Higher in UI contexts Lower (if properly awaited)

Bonus tip: get the best out of AggregateException

We can expand a bit on the AggregateException type.

That specific type of exception acts as a container for all the exceptions thrown when using Task.WaitAll.

It contains a property named InnerExceptions that contains all the exceptions thrown so that you can access them using an Enumerator.

A common example is this:

if (ex is AggregateException aggEx)
{
    Console.WriteLine("There are {0} exceptions in the aggregate exception.", aggEx.InnerExceptions.Count);
    foreach (var innerEx in aggEx.InnerExceptions)
    {
        Console.WriteLine("Inner exception: {0}", innerEx.Message);
    }
}

Further readings

This article is all about handling the unhappy path.

If you want to learn more about Task.WaitAll and Task.WhenAll, I’d suggest you read the following two articles that I find totally interesting and well-written:

πŸ”— Understanding Task.WaitAll and Task.WhenAll in C# | Muhammad Umair

and

πŸ”— Understanding WaitAll and WhenAll in .NET | Prasad Raveendran

This article first appeared on Code4IT 🐧

But, if you don’t know what asynchronous programming is and how to use TAP in C#, I’d suggest you start from the basics with this article:

πŸ”— First steps with asynchronous programming in C# | Code4IT

Wrapping up

I hope you enjoyed this article! Let’s keep in touch on LinkedIn, Twitter or BlueSky! πŸ€œπŸ€›

Happy coding!

🐧

About the author

Davide Bellone is a Principal Backend Developer with more than 10 years of professional experience with Microsoft platforms and frameworks.

He loves learning new things and sharing these learnings with others: that’s why he writes on this blog and is involved as speaker at tech conferences.

He's a Microsoft MVP πŸ†, conference speaker (here's his Sessionize Profile) and content creator on LinkedIn.