Introduction
Multiple await statements and Task.WaitAll are not equivalent. Sequential await calls execute tasks one after another, while Task.WhenAll (the async version of Task.WaitAll) runs tasks concurrently. Task.WaitAll blocks the calling thread synchronously and can cause deadlocks in UI and ASP.NET contexts. Use await Task.WhenAll() for concurrent async execution, and sequential await only when each task depends on the previous one's result.
Sequential Awaits (One at a Time)
Each await pauses until the task completes before starting the next:
1async Task SequentialExample()
2{
3 var result1 = await GetDataFromApiAsync("users"); // 2 seconds
4 var result2 = await GetDataFromApiAsync("orders"); // 2 seconds
5 var result3 = await GetDataFromApiAsync("products"); // 2 seconds
6
7 // Total time: ~6 seconds (2 + 2 + 2)
8 Console.WriteLine($"{result1}, {result2}, {result3}");
9}
Each call waits for the previous one to finish. Use this when tasks depend on each other (e.g., result1 is needed to start task2).
Task.WhenAll (Concurrent Async)
Start all tasks first, then await them together:
1async Task ConcurrentExample()
2{
3 var task1 = GetDataFromApiAsync("users"); // Starts immediately
4 var task2 = GetDataFromApiAsync("orders"); // Starts immediately
5 var task3 = GetDataFromApiAsync("products"); // Starts immediately
6
7 await Task.WhenAll(task1, task2, task3);
8 // Total time: ~2 seconds (all run in parallel)
9
10 Console.WriteLine($"{task1.Result}, {task2.Result}, {task3.Result}");
11}
All three tasks start concurrently. Task.WhenAll completes when the slowest task finishes.
Getting Results from WhenAll
1async Task<string[]> GetAllDataAsync()
2{
3 var tasks = new[]
4 {
5 GetDataFromApiAsync("users"),
6 GetDataFromApiAsync("orders"),
7 GetDataFromApiAsync("products")
8 };
9
10 // WhenAll returns results in the same order as the input tasks
11 string[] results = await Task.WhenAll(tasks);
12 return results;
13}
Task.WaitAll (Synchronous Block — Avoid)
Task.WaitAll blocks the calling thread until all tasks complete:
1void BlockingExample()
2{
3 var task1 = GetDataFromApiAsync("users");
4 var task2 = GetDataFromApiAsync("orders");
5
6 // BLOCKS the current thread — can deadlock in UI/ASP.NET contexts
7 Task.WaitAll(task1, task2);
8
9 Console.WriteLine(task1.Result);
10}
This is dangerous because it blocks the thread, which can cause deadlocks when the awaited tasks need the same synchronization context (common in WPF, WinForms, and older ASP.NET).
Side-by-Side Comparison
1// Pattern 1: Sequential awaits (~6 seconds)
2var a = await DoWorkAsync(2000);
3var b = await DoWorkAsync(2000);
4var c = await DoWorkAsync(2000);
5
6// Pattern 2: Task.WhenAll (~2 seconds, concurrent, async)
7var taskA = DoWorkAsync(2000);
8var taskB = DoWorkAsync(2000);
9var taskC = DoWorkAsync(2000);
10await Task.WhenAll(taskA, taskB, taskC);
11
12// Pattern 3: Task.WaitAll (~2 seconds, concurrent, BLOCKING)
13var taskX = DoWorkAsync(2000);
14var taskY = DoWorkAsync(2000);
15Task.WaitAll(taskX, taskY); // Blocks thread — avoid
| Pattern | Concurrent | Async | Thread Blocked | Risk |
Sequential await | No | Yes | No | None |
await Task.WhenAll | Yes | Yes | No | None |
Task.WaitAll | Yes | No | Yes | Deadlock |
Task.WhenAny | Yes | Yes | No | None |
Error Handling
1// Sequential: first exception stops execution
2try
3{
4 var a = await MayFailAsync("A"); // If this throws, B never runs
5 var b = await MayFailAsync("B");
6}
7catch (Exception ex)
8{
9 Console.WriteLine(ex.Message);
10}
11
12// WhenAll: all tasks run; all exceptions are collected
13try
14{
15 await Task.WhenAll(MayFailAsync("A"), MayFailAsync("B"), MayFailAsync("C"));
16}
17catch (Exception ex)
18{
19 // 'ex' is the first exception, but all are available:
20 // The AggregateException is in the Task.WhenAll task
21}
22
23// Access all exceptions from WhenAll
24var allTasks = Task.WhenAll(MayFailAsync("A"), MayFailAsync("B"));
25try
26{
27 await allTasks;
28}
29catch
30{
31 foreach (var ex in allTasks.Exception.InnerExceptions)
32 {
33 Console.WriteLine($"Error: {ex.Message}");
34 }
35}
Task.WhenAny
Wait for the first task to complete:
1async Task<string> GetFastestResultAsync()
2{
3 var task1 = FetchFromServer1Async();
4 var task2 = FetchFromServer2Async();
5
6 var fastest = await Task.WhenAny(task1, task2);
7 return await fastest; // Get the result of whichever finished first
8}
Real-World Example
1public class DashboardService
2{
3 public async Task<DashboardData> LoadDashboardAsync(int userId)
4 {
5 // These are independent — run concurrently
6 var profileTask = _userService.GetProfileAsync(userId);
7 var ordersTask = _orderService.GetRecentOrdersAsync(userId);
8 var notificationsTask = _notificationService.GetUnreadAsync(userId);
9
10 await Task.WhenAll(profileTask, ordersTask, notificationsTask);
11
12 return new DashboardData
13 {
14 Profile = profileTask.Result,
15 RecentOrders = ordersTask.Result,
16 Notifications = notificationsTask.Result
17 };
18 }
19}
Common Pitfalls
Assuming sequential await and Task.WhenAll are equivalent: Sequential awaits run tasks one after another (total time = sum). Task.WhenAll runs them concurrently (total time = max). For independent tasks, WhenAll is significantly faster.
Using Task.WaitAll instead of await Task.WhenAll: WaitAll blocks the calling thread synchronously, which causes deadlocks in UI applications (WPF, WinForms) and legacy ASP.NET. Always use await Task.WhenAll in async code.
Starting tasks inside the await call: Writing await Task.WhenAll(await task1, await task2) awaits each task sequentially before passing results to WhenAll. Start tasks without await first, then pass the Task objects to WhenAll.
Not handling AggregateException from Task.WhenAll: When multiple tasks in WhenAll fail, only the first exception is thrown by await. To access all exceptions, inspect task.Exception.InnerExceptions on the WhenAll task.
Using .Result or .Wait() on an incomplete task: Accessing .Result on a task that has not completed blocks the thread, just like Task.WaitAll. Only access .Result after await Task.WhenAll has completed to ensure the tasks are finished.
Summary
Sequential await runs tasks one at a time — use when tasks depend on each other
await Task.WhenAll() runs tasks concurrently — use for independent tasks
Task.WaitAll() blocks the thread and risks deadlocks — avoid in async code
Start all tasks before awaiting to enable concurrency
Use Task.WhenAny() to get the result of the first task to complete