Task.WhenAll
asynchronous programming
C#
performance optimization
async await

Why should I prefer single 'await Task.WhenAll' over multiple awaits?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

await Task.WhenAll(task1, task2, task3) runs all tasks concurrently and waits for all to complete. Multiple sequential awaits (await task1; await task2; await task3;) run tasks one after another. If the tasks are independent (no data dependencies), Task.WhenAll is faster because tasks overlap. It also handles exceptions better — Task.WhenAll collects all exceptions into an AggregateException, while sequential awaits stop at the first failure and lose the others.

The Performance Difference

csharp
1// Sequential awaits — total time = sum of all tasks
2async Task SequentialAsync()
3{
4    var result1 = await GetDataFromApi1Async();  // 2 seconds
5    var result2 = await GetDataFromApi2Async();  // 3 seconds
6    var result3 = await GetDataFromApi3Async();  // 1 second
7    // Total: ~6 seconds
8}
9
10// Task.WhenAll — total time = slowest task
11async Task ConcurrentAsync()
12{
13    var task1 = GetDataFromApi1Async();  // Started immediately
14    var task2 = GetDataFromApi2Async();  // Started immediately
15    var task3 = GetDataFromApi3Async();  // Started immediately
16
17    await Task.WhenAll(task1, task2, task3);
18    // Total: ~3 seconds (the slowest task)
19
20    var result1 = task1.Result;
21    var result2 = task2.Result;
22    var result3 = task3.Result;
23}

The key is to start all tasks before awaiting any of them. Task.WhenAll then waits for all of them to finish.

Getting Results from Task.WhenAll

csharp
1// When all tasks return the same type
2async Task<int[]> GetAllCountsAsync()
3{
4    var tasks = new[]
5    {
6        GetUserCountAsync(),
7        GetOrderCountAsync(),
8        GetProductCountAsync()
9    };
10
11    int[] results = await Task.WhenAll(tasks);
12    // results[0] = user count, results[1] = order count, etc.
13    return results;
14}
15
16// When tasks return different types
17async Task LoadDashboardAsync()
18{
19    var usersTask = GetUsersAsync();       // Task<List<User>>
20    var ordersTask = GetOrdersAsync();     // Task<List<Order>>
21    var statsTask = GetStatsAsync();       // Task<DashboardStats>
22
23    await Task.WhenAll(usersTask, ordersTask, statsTask);
24
25    var users = usersTask.Result;
26    var orders = ordersTask.Result;
27    var stats = statsTask.Result;
28}

Exception Handling

csharp
1// Sequential: stops at first exception, loses the rest
2async Task SequentialWithErrors()
3{
4    try
5    {
6        await Task1Async();  // Throws
7        await Task2Async();  // Never runs
8        await Task3Async();  // Never runs
9    }
10    catch (Exception ex)
11    {
12        // Only sees the exception from Task1
13    }
14}
15
16// Task.WhenAll: runs all tasks, collects all exceptions
17async Task ConcurrentWithErrors()
18{
19    var task1 = Task1Async();  // Throws
20    var task2 = Task2Async();  // Also throws
21    var task3 = Task3Async();  // Succeeds
22
23    try
24    {
25        await Task.WhenAll(task1, task2, task3);
26    }
27    catch (Exception ex)
28    {
29        // ex is the first exception (task1's)
30        // But all exceptions are available:
31        if (task1.IsFaulted) Console.WriteLine(task1.Exception.InnerException.Message);
32        if (task2.IsFaulted) Console.WriteLine(task2.Exception.InnerException.Message);
33        if (task3.IsCompletedSuccessfully) Console.WriteLine("Task3 succeeded");
34    }
35}

When to Use Sequential Awaits

Sequential awaits are correct when tasks depend on each other:

csharp
1// CORRECT: task2 needs result from task1
2async Task DependentTasks()
3{
4    var user = await GetUserAsync(userId);          // Must complete first
5    var orders = await GetOrdersAsync(user.Id);     // Depends on user
6    var details = await GetOrderDetailsAsync(orders.First().Id);  // Depends on orders
7}

Real-World Example: Parallel API Calls

csharp
1public class DashboardService
2{
3    private readonly HttpClient _http;
4
5    public async Task<Dashboard> LoadDashboardAsync()
6    {
7        var usersTask = _http.GetFromJsonAsync<List<User>>("/api/users");
8        var ordersTask = _http.GetFromJsonAsync<List<Order>>("/api/orders");
9        var revenueTask = _http.GetFromJsonAsync<Revenue>("/api/revenue");
10        var notificationsTask = _http.GetFromJsonAsync<List<Notification>>("/api/notifications");
11
12        await Task.WhenAll(usersTask, ordersTask, revenueTask, notificationsTask);
13
14        return new Dashboard
15        {
16            Users = usersTask.Result,
17            Orders = ordersTask.Result,
18            Revenue = revenueTask.Result,
19            Notifications = notificationsTask.Result
20        };
21    }
22}

Task.WhenAll with Timeout

csharp
1async Task WithTimeout()
2{
3    var dataTask = Task.WhenAll(
4        GetApi1Async(),
5        GetApi2Async(),
6        GetApi3Async()
7    );
8
9    var timeoutTask = Task.Delay(TimeSpan.FromSeconds(10));
10
11    var completed = await Task.WhenAny(dataTask, timeoutTask);
12
13    if (completed == timeoutTask)
14        throw new TimeoutException("API calls took too long");
15
16    await dataTask;  // Get results or propagate exceptions
17}

Common Pitfalls

  • Awaiting each task immediately instead of starting all first: await Task1Async(); await Task2Async(); runs sequentially. To run concurrently, store the tasks in variables first (var t1 = Task1Async(); var t2 = Task2Async();) and then await Task.WhenAll(t1, t2). The task starts when you call the method, not when you await it.
  • Using Task.WhenAll for dependent tasks: If task2 needs the result of task1, they cannot run concurrently. Using Task.WhenAll in this case either fails (because task2 needs input that does not exist yet) or requires restructuring the dependency chain.
  • Ignoring exceptions from individual tasks: await Task.WhenAll(...) throws only the first exception. If multiple tasks fail, check each task's .Exception property individually to log all failures. The AggregateException is accessible via the Task.WhenAll task's .Exception property.
  • Using .Result before Task.WhenAll completes: Accessing .Result on a task that has not completed blocks the calling thread synchronously. Always await Task.WhenAll(...) first, then access .Result on the completed tasks.
  • Creating tasks in a loop without collecting them: foreach (var item in items) await ProcessAsync(item); processes items sequentially. Collect tasks into a list first: var tasks = items.Select(ProcessAsync).ToList(); await Task.WhenAll(tasks); for concurrent processing.

Summary

  • Use await Task.WhenAll(...) for independent tasks to run them concurrently (total time = slowest task)
  • Use sequential await only when tasks depend on each other's results
  • Start all tasks before awaiting any — the task begins when you call the async method
  • Task.WhenAll collects all exceptions; sequential awaits stop at the first failure
  • Access .Result only after Task.WhenAll has completed to avoid synchronous blocking

Course illustration
Course illustration

All Rights Reserved.