Task.WhenAll, Task.WhenAny, and Task.Run in C#
Task.WhenAll runs multiple tasks concurrently and waits for all of them to finish. Task.WhenAny returns as soon as the first task completes. Task.Run schedules synchronous, CPU-bound work on a thread pool thread.
Choosing the right one depends on whether you need all results, just the fastest result, or need to move CPU work off the calling thread.
Task.WhenAll — Wait for All Tasks
Task.WhenAll starts all tasks at once and suspends until every task has completed. The total time is determined by the slowest task, not the sum of all tasks.
using System;
using System.Diagnostics;
using System.Net.Http;
using System.Threading.Tasks;
// Sequential: total time ≈ 1000 + 500 + 800 = 2300 ms
async Task RunSequential()
{
await Task.Delay(1000);
await Task.Delay(500);
await Task.Delay(800);
Console.WriteLine("Sequential: ~2300 ms");
}
// Concurrent: total time ≈ max(1000, 500, 800) = 1000 ms
async Task RunConcurrent()
{
var sw = Stopwatch.StartNew();
await Task.WhenAll(
Task.Delay(1000),
Task.Delay(500),
Task.Delay(800)
);
Console.WriteLine($"Concurrent: ~{sw.ElapsedMilliseconds} ms"); // ~1000 ms
}Collecting Results with Task.WhenAll
When all tasks return the same type, Task.WhenAll returns an array of results in the same order the tasks were passed in.
static async Task<int> FetchUserCountAsync(int regionId)
{
await Task.Delay(300); // simulate DB query
return regionId * 100;
}
static async Task CollectResults()
{
Task<int> region1 = FetchUserCountAsync(1);
Task<int> region2 = FetchUserCountAsync(2);
Task<int> region3 = FetchUserCountAsync(3);
int[] counts = await Task.WhenAll(region1, region2, region3);
Console.WriteLine($"Region 1: {counts[0]}"); // 100
Console.WriteLine($"Region 2: {counts[1]}"); // 200
Console.WriteLine($"Region 3: {counts[2]}"); // 300
Console.WriteLine($"Total: {counts[0] + counts[1] + counts[2]}"); // 600
}Exception Handling with Task.WhenAll
When one or more tasks fail, await Task.WhenAll(...) re-throws the first exception. To inspect all failures, catch the Task before awaiting and read its Exception.InnerExceptions.
static async Task HandleMultipleExceptions()
{
Task t1 = Task.FromException(new InvalidOperationException("Region 1 failed"));
Task t2 = Task.FromException(new TimeoutException("Region 2 timed out"));
Task t3 = Task.Delay(200); // succeeds
Task allTasks = Task.WhenAll(t1, t2, t3);
try
{
await allTasks;
}
catch
{
// allTasks.Exception contains all failures
foreach (var ex in allTasks.Exception!.InnerExceptions)
{
Console.WriteLine($"Error: {ex.Message}");
}
// Output:
// Error: Region 1 failed
// Error: Region 2 timed out
}
}Task.WhenAny — First Completed Task
Task.WhenAny returns a Task<Task> that completes as soon as the first task finishes. The returned value is the completed task, which you then await to get its result or exception.
static async Task<string> QueryPrimaryAsync()
{
await Task.Delay(800);
return "primary result";
}
static async Task<string> QueryReplicaAsync()
{
await Task.Delay(300);
return "replica result";
}
static async Task UseWhenAny()
{
Task<string> primary = QueryPrimaryAsync();
Task<string> replica = QueryReplicaAsync();
// Returns whichever task finishes first
Task<string> winner = await Task.WhenAny(primary, replica);
string result = await winner; // await the winning task to unwrap the result
Console.WriteLine($"Got: {result}"); // Got: replica result (faster)
}Timeout Pattern with Task.WhenAny
Combine Task.WhenAny with Task.Delay to implement a timeout without a CancellationToken.
static async Task<string> FetchWithTimeoutAsync()
{
Task<string> dataTask = FetchSlowDataAsync();
Task timeoutTask = Task.Delay(TimeSpan.FromSeconds(2));
Task completedFirst = await Task.WhenAny(dataTask, timeoutTask);
if (completedFirst == timeoutTask)
{
throw new TimeoutException("Operation timed out after 2 seconds.");
}
return await dataTask; // safe — we know it completed
}
static async Task<string> FetchSlowDataAsync()
{
await Task.Delay(3000); // slower than the timeout
return "slow data";
}Parallel Async Work with a List
Use Select + Task.WhenAll to fan out over a collection and gather results.
using System.Collections.Generic;
using System.Linq;
static async Task<IEnumerable<string>> FetchAllPagesAsync(IEnumerable<string> urls)
{
using HttpClient client = new HttpClient();
IEnumerable<Task<string>> fetchTasks = urls.Select(url =>
client.GetStringAsync(url));
string[] pages = await Task.WhenAll(fetchTasks);
return pages;
}
// Usage
List<string> urls = new()
{
"https://example.com/page1",
"https://example.com/page2",
"https://example.com/page3",
};
IEnumerable<string> results = await FetchAllPagesAsync(urls);
Console.WriteLine($"Fetched {results.Count()} pages");Task.Run — Offload CPU-Bound Work
Task.Run schedules a delegate on the thread pool. Use it when you have synchronous, CPU-intensive code and want to keep the calling thread (UI thread or ASP.NET request thread) free.
static int HeavyComputation(int n)
{
// Simulate CPU work
int result = 0;
for (int i = 0; i < n; i++) result += i % 7;
return result;
}
static async Task RunCpuWork()
{
// Offload to thread pool — calling thread is released
int result = await Task.Run(() => HeavyComputation(10_000_000));
Console.WriteLine($"Result: {result}");
}Combining Task.Run with Task.WhenAll
Run multiple CPU-bound operations concurrently by combining Task.Run with Task.WhenAll.
static async Task RunParallelCpuWork()
{
Task<int> job1 = Task.Run(() => HeavyComputation(5_000_000));
Task<int> job2 = Task.Run(() => HeavyComputation(8_000_000));
Task<int> job3 = Task.Run(() => HeavyComputation(3_000_000));
int[] results = await Task.WhenAll(job1, job2, job3);
Console.WriteLine($"Jobs completed: {results[0]}, {results[1]}, {results[2]}");
}Comparison: WhenAll vs WhenAny vs Task.Run
Task.WhenAll | Task.WhenAny | Task.Run | |
|---|---|---|---|
| Completes when | All tasks finish | First task finishes | Scheduled work completes |
| Best for | Fan-out async I/O, collecting results | Race patterns, timeouts, first-available | CPU-bound work off calling thread |
| Returns | Task / Task<T[]> | Task<Task> / Task<Task<T>> | Task / Task<T> |
| Exception behavior | Throws first; all failures in .Exception | Throws if winner faulted | Throws as AggregateException |
| Cancellation | Pass CancellationToken to each task | Check if winner is cancelled task | Pass CancellationToken to Task.Run |
Key Takeaways
- Use
Task.WhenAllwhen you need all results and want them concurrently. - Use
Task.WhenAnyfor race patterns — timeout, first-available replica, progress tracking. - Use
Task.Runto move CPU-bound synchronous code off the calling thread; avoid it for I/O tasks that already have async APIs. - Always
awaitthe task returned byTask.WhenAnya second time to propagate exceptions from the winner. - To capture all exceptions from
Task.WhenAll, store the task before awaiting and inspect.Exception.InnerExceptions.