Beyond the Basics: Handling Task.WhenAll() Exceptions

In this post, we'll delve into how to handle exceptions when Task.WhenAll() throws them. This blog post was inspired by a great LinkedIn post from Habib ul Rehman. I wanted to go “Beyond The Basics,” so here is my deeper dive into Task.WhenAll() and what to do when things don’t go quite as planned.

Exception Thrown Now What To Do?

If one or more tasks throw exceptions, Task.WhenAll() aggregates these exceptions into an AggregateException. Here's how you handle them:


try
{
    await Task.WhenAll(task1, task2);
}
catch (AggregateException ex)
{
    foreach (var innerException in ex.InnerExceptions)
    {
        Console.WriteLine(innerException.Message);
    }
}
                            

This approach allows you to catch and handle each individual exception thrown by the tasks. It’s essential to remember that AggregateException encapsulates all exceptions from the tasks, so iterating through InnerExceptions gives you a clear view of what went wrong.

Alternative One:   Functional Programming Paradigm (Gaining Popularity)

Of course, there are numerous ways to throw and catch exceptions. In functional programming paradigms, you can use Result or Either types to handle errors without throwing exceptions. These types encapsulate both success and failure states, promoting a more predictable and controlled error handling mechanism.

However, for this post, I'll stick with Object-Oriented Programming (OOP).

Alternative Two:   Catch Exception

Below is code to catch the exceptions

Example Code

Here is some test code to play with:


Task task1 = Task.Run(() => { throw new InvalidOperationException("Task 1 failed."); });
Task task2 = Task.Run(() => { throw new ArgumentNullException("Task 2 failed."); });

var tasks = new[] { task1, task2 };

try
{
    await Task.WhenAll(tasks);
}
catch (AggregateException)
{
    foreach (var task in tasks)
    {
        if (task.Exception != null)
        {
            foreach (var innerException in task.Exception.InnerExceptions)
            {
                Console.WriteLine(innerException.Message);
            }
        }
    }
}
                            

Extending Exception Handling with ContinueWith

You can extend this approach with the ContinueWith method to handle each exception individually as tasks complete:


Task task1 = Task.Run(() => { throw new InvalidOperationException("Task 1 failed."); }).ContinueWith(t =>
{
    if (t.IsFaulted)
    {
        Console.WriteLine(t.Exception.InnerException.Message);
    }
});

Task task2 = Task.Run(() => { throw new ArgumentNullException("Task 2 failed."); }).ContinueWith(t =>
{
    if (t.IsFaulted)
    {
        Console.WriteLine(t.Exception.InnerException.Message);
    }
});

await Task.WhenAll(task1, task2);
                            

Using ContinueWith, you attach a continuation that handles the faulted state of each task, providing a more granular level of control over exception handling.

Conclusion

Handling multiple asynchronous tasks in .NET can feel like juggling, but with the right techniques, you can manage exceptions effectively. Whether you stick with traditional OOP approaches or explore functional programming paradigms, understanding how to handle exceptions with Task.WhenAll() is crucial for building robust and reliable applications.

Happy coding, and may your tasks complete successfully without exception! 🚀💻