1. Task.WhenAll: wait for all at once
In real life it's rare that you only do one thing. You get out of bed while checking the time, take a shower and sing, get dressed and listen to music while making coffee. Or when working on a computer you might download several files at once, send a bunch of network requests, calculate lots of data in different directions. All these actions can (and should!) run in parallel so you don't waste the user's time. We don't want to wait for each task to finish one by one! How to organize such an "orchestra"? How to know when EVERYTHING is done? Or, conversely, find out who finished first?
Task.WhenAll and Task.WhenAny are the tools you need for these scenarios.
WhenAll method
Task.WhenAll is a static method of the Task class that accepts a collection of tasks and returns a new task that completes only when all provided tasks have finished. It's like waiting at an exam until all your classmates hand in their tickets before leaving the room.
Method signature
Task Task.WhenAll(params Task[] tasks)
Task<TResult[]> Task.WhenAll<TResult>(params Task<TResult>[] tasks)
There are versions for tasks that return results (Task<TResult>) and for "void" ones (Task).
Simple example: waiting for all files to download
Let's try to download three files asynchronously. For simplicity — we'll simulate downloads with a delay (Task.Delay). Examples are realistic; code is runnable.
using System;
using System.Threading.Tasks;
class Program
{
// Simulate file download with a delay, return the name
static async Task<string> DownloadFileAsync(string fileName)
{
Console.WriteLine($"► Starting download: {fileName}");
await Task.Delay(2000); // Wait 2 seconds
Console.WriteLine($"✓ Download finished: {fileName}");
return fileName;
}
static async Task Main()
{
var files = new[] { "fileA.txt", "fileB.txt", "fileC.txt" };
// Start all downloads at the same time
var downloadTasks = new Task<string>[files.Length];
for (int i = 0; i < files.Length; i++)
{
downloadTasks[i] = DownloadFileAsync(files[i]);
}
// Wait for all downloads to complete
string[] results = await Task.WhenAll(downloadTasks);
Console.WriteLine($"All downloads completed! List: {string.Join(", ", results)}");
}
}
What's happening here?
- We don't wait for each task one by one. All three are started in parallel (asynchronously).
- Task.WhenAll(downloadTasks) returns a task that will complete only when all tasks complete.
- After that we can work with results of all tasks — use them, print them to the console, send them further.
Analogy
It's like assigning three couriers to deliver three packages and telling your boss only when all of them have successfully reached the clients.
How Task.WhenAll works
sequenceDiagram
participant Program
participant Task1
participant Task2
participant Task3
Program->>Task1: Start
Program->>Task2: Start
Program->>Task3: Start
Task1-->>Program: Completed (may finish before others)
Task2-->>Program: Completed
Task3-->>Program: Completed
Program->>Program: Task.WhenAll completed, all results available
What if one of the tasks fails?
Task.WhenAll doesn't stop when one task fails. It waits for all tasks to finish. If at least one threw an exception — the resulting task will be in Faulted state, and it will contain a collection of all exceptions that occurred.
Example
static async Task<string> MayThrowAsync(string fileName)
{
await Task.Delay(500);
if (fileName == "fileB.txt")
throw new Exception("Error downloading fileB.txt");
return fileName;
}
static async Task Main()
{
var files = new[] { "fileA.txt", "fileB.txt", "fileC.txt" };
var downloadTasks = files.Select(MayThrowAsync).ToArray();
try
{
string[] results = await Task.WhenAll(downloadTasks);
Console.WriteLine($"All good: {string.Join(", ", results)}");
}
catch (Exception ex)
{
Console.WriteLine($"At least one task finished with an error: {ex.Message}");
}
}
In case of an exception, to inspect all inner errors use AggregateException.InnerExceptions.
Official docs: Task.WhenAll docs
2. Task.WhenAny: wait for the first completed task
Task.WhenAny is also a static method, but it completes as soon as any of the provided tasks transitions to a "Completed" state (regardless of success or failure). It returns a reference to the first task that completed.
Signature
Task<Task> Task.WhenAny(params Task[] tasks)
Task<Task<TResult>> Task.WhenAny<TResult>(params Task<TResult>[] tasks)
Analogy
Whoever bakes the pie first — kudos to them, you can ask the others to stop. Sometimes it's enough to know which of several servers answered fastest and use that result.
Example: who's faster?
using System;
using System.Threading.Tasks;
class Program
{
// Simulate request with different speed
static async Task<string> RequestAsync(string name, int delay)
{
await Task.Delay(delay);
return $"{name} finished in {delay} ms";
}
static async Task Main()
{
var taskA = RequestAsync("A", 1000);
var taskB = RequestAsync("B", 700); // Fastest
var taskC = RequestAsync("C", 1500);
// Wait for the first completed task
Task<string> finished = await Task.WhenAny(taskA, taskB, taskC);
Console.WriteLine($"First finished: {finished.Result}");
}
}
With WhenAny we find out which task got to the finish line first. After that you can cancel the other tasks, for example using CancellationToken (topic of later lectures).
Why does WhenAny return a task and not a result?
Because the method doesn't know what result type the tasks have: it can't know in advance which task will finish first. So it returns the task itself — then you need to extract the result yourself: via await or the Result property on that task.
WhenAll vs WhenAny: comparative illustration
| Method | When it triggers | What it returns | Typical scenario |
|---|---|---|---|
|
When all have finished | A task (Task/Task<T[]>) | Wait for all responses or files, aggregate results |
|
When any one of the tasks has finished | The task of the very first completed task | Use the first result, cancel the rest |
3. Combining WhenAll and WhenAny — real-life scenarios
Sometimes you need to wait for any task first, and then for all. Or the other way around.
Example: "ping-pong" between two servers
Imagine you send requests to two server clones in case one is slow. But you use whichever result comes first.
static async Task Main()
{
var fastServer = RequestAsync("Fast server", 400); // 400 ms
var slowServer = RequestAsync("Slow server", 2000); // 2000 ms
var completed = await Task.WhenAny(fastServer, slowServer);
Console.WriteLine(await completed);
// *Optionally*: here you can cancel remaining incomplete tasks via CancellationToken
}
Example: start processing only after all data is loaded
Your app has three data sources. Only when all three are loaded should you move to processing:
static async Task Main()
{
var task1 = DownloadFileAsync("a.txt");
var task2 = DownloadFileAsync("b.txt");
var task3 = DownloadFileAsync("c.txt");
var all = await Task.WhenAll(task1, task2, task3);
ProcessFiles(all[0], all[1], all[2]);
}
4. Common mistakes when working with Task.WhenAll and Task.WhenAny
Mistake #1: expecting WhenAll to finish when the first task completes.
Beginners think that Task.WhenAll will complete as soon as the first task finishes. In reality it waits for all tasks to complete.
Mistake #2: ignoring exceptions in WhenAll.
If one of the tasks throws an exception, the resulting task will be in Faulted state. If you don't handle AggregateException.InnerExceptions, you might miss important errors.
Mistake #3: accessing Result without checks on WhenAny.
If the first completed task failed, calling Result will throw. Check Task.IsFaulted before accessing it.
Mistake #4: starting tasks sequentially instead of in parallel.
For example, awaiting each task inside a loop instead of using Task.WhenAll drastically reduces performance.
GO TO FULL VERSION