Task.WhenAll and Task.WhenAny are the two fundamental combinators for concurrent async operations. They serve different purposes, and using the right one (or combining them) can dramatically improve both performance and resilience.
Task.WhenAll — Wait for Everything
Task.WhenAll returns a task that completes when all input tasks have completed. Use it when you need all results before proceeding:
var userTask = _userService.GetByIdAsync(userId);
var ordersTask = _orderService.GetByUserIdAsync(userId);
var prefsTask = _prefsService.GetAsync(userId);
await Task.WhenAll(userTask, ordersTask, prefsTask);
var model = new DashboardViewModel
{
User = userTask.Result,
Orders = ordersTask.Result,
Preferences = prefsTask.Result
};
All three queries run concurrently. If each takes 200ms, the total wall time is roughly 200ms rather than 600ms.
Important: start all tasks before awaiting any of them. A common mistake is to await each task as it is created, which serialises the operations:
// Wrong — these run sequentially
var user = await _userService.GetByIdAsync(userId);
var orders = await _orderService.GetByUserIdAsync(userId);
var prefs = await _prefsService.GetAsync(userId);
Error Handling with WhenAll
Task.WhenAll waits for all tasks to complete, even if some fail. It then throws the first exception. To see all exceptions, inspect the returned task:
var allTask = Task.WhenAll(tasks);
try
{
await allTask;
}
catch
{
// allTask.Exception is an AggregateException containing all failures
foreach (var ex in allTask.Exception!.InnerExceptions)
{
_logger.LogError(ex, "Task failed");
}
}
Task.WhenAny — First One Wins
Task.WhenAny returns when the first task completes. It is useful for timeouts, redundant requests, and progress monitoring.
Timeout Pattern
public static async Task<T> WithTimeout<T>(Task<T> task, TimeSpan timeout)
{
var delayTask = Task.Delay(timeout);
var winner = await Task.WhenAny(task, delayTask);
if (winner == delayTask)
{
throw new TimeoutException(
$"Operation timed out after {timeout.TotalSeconds}s");
}
return await task; // Propagates exceptions if the task faulted
}
// Usage
var result = await WithTimeout(
httpClient.GetStringAsync(url),
TimeSpan.FromSeconds(5));
Note that Task.WhenAny does not cancel the losing task. You need cancellation tokens for that:
public static async Task<T> WithTimeout<T>(
Func<CancellationToken, Task<T>> operation,
TimeSpan timeout)
{
using var cts = new CancellationTokenSource(timeout);
try
{
return await operation(cts.Token);
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
throw new TimeoutException($"Operation timed out after {timeout.TotalSeconds}s");
}
}
Redundant Request Pattern
Send the same request to multiple endpoints and use whichever responds first:
public async Task<WeatherData> GetWeatherAsync(double lat, double lon)
{
using var cts = new CancellationTokenSource();
var primary = FetchFromPrimaryAsync(lat, lon, cts.Token);
var fallback = FetchFromFallbackAsync(lat, lon, cts.Token);
var winner = await Task.WhenAny(primary, fallback);
cts.Cancel(); // Cancel the slower request
return await winner; // Unwrap result or propagate exception
}
Processing as Completed
Process tasks in completion order rather than creation order:
var pending = new List<Task<DownloadResult>>(downloadTasks);
while (pending.Count > 0)
{
var completed = await Task.WhenAny(pending);
pending.Remove(completed);
var result = await completed;
Console.WriteLine($"Downloaded: {result.FileName} ({result.Size} bytes)");
}
This gives you immediate feedback as each download finishes, rather than waiting for all of them.
Combining Both
A common pattern is to combine WhenAll with a timeout using WhenAny:
var allTasks = Task.WhenAll(tasks);
var timeout = Task.Delay(TimeSpan.FromSeconds(30));
var winner = await Task.WhenAny(allTasks, timeout);
if (winner == timeout)
{
_logger.LogWarning("Not all tasks completed within 30 seconds");
// Handle partial results from completed tasks
var completedResults = tasks
.Where(t => t.IsCompletedSuccessfully)
.Select(t => t.Result)
.ToList();
}
.NET 9: Task.WhenEach
.NET 9 introduced Task.WhenEach, which returns an IAsyncEnumerable<Task<T>> yielding tasks in completion order. This replaces the manual WhenAny loop pattern:
await foreach (var completed in Task.WhenEach(downloadTasks))
{
try
{
var result = await completed;
Console.WriteLine($"Downloaded: {result.FileName}");
}
catch (Exception ex)
{
Console.WriteLine($"Download failed: {ex.Message}");
}
}
This is cleaner, avoids the O(n^2) cost of repeated WhenAny calls on a shrinking list, and handles exceptions per-task naturally.
Choosing the Right Combinator
Use Task.WhenAll when you need all results and want concurrent execution. Use Task.WhenAny when you want the first result — for timeouts, racing, or progress reporting. Use Task.WhenEach (in .NET 9+) when you want to process results as they arrive without manually tracking a pending list. And always remember: starting tasks concurrently and combining them is almost always faster than sequential awaits.