C# & .NET (C# 14, .NET 10 LTS) Module 15 – Asynchronous programming
Composing tasks: WhenAll, WhenAny and WhenEach
Run C# tasks side by side with Task.WhenAll, race them with WhenAny, take each result as it arrives with WhenEach, and add timeouts with WaitAsync.
What you will learn
- Combine the results of tasks that run at the same time with Task.WhenAll
- Use WhenAny for the first task to finish, and WhenEach for every task in the order it finishes
- Find every exception of a failed WhenAll, and skip a failed winner of WhenAny
- Add a timeout to any task with WaitAsync, and say what keeps running after it
- Write a search that takes answers as they arrive, stops early and gives up at a time limit
Before you start
What you’ll learn · Before you start
What you will learn
- Combine the results of tasks that run at the same time with Task.WhenAll
- Use WhenAny for the first task to finish, and WhenEach for every task in the order it finishes
- Find every exception of a failed WhenAll, and skip a failed winner of WhenAny
- Add a timeout to any task with WaitAsync, and say what keeps running after it
- Write a search that takes answers as they arrive, stops early and gives up at a time limit
Before you start
On this page
Three price quotes that arrive after 300, 100 and 200 ms can be read in three ways: all of them at once after 300 ms,
the first one after 100 ms, or each one as it arrives, at 100, 200 and 300 ms. Task.WhenAll, Task.WhenAny and
Task.WhenEach are those three ways, and each returns something you await.
You will combine running tasks with each of them, read a recorded run of all three side by side, and see where
WaitAsync puts a limit on the wait.
WhenAll waits for every task and keeps the order you gave
whenall.cs starts three quote calls, then awaits Task.WhenAll of the three tasks. Each call prints when its answer
arrives:
// Task.WhenAll: one task that completes when every task given to it has completed. Its result is an array of the
// results, in the order the tasks were passed, whatever order they finished in.
Task<int> north = QuoteAsync("north", 412, 300);
Task<int> south = QuoteAsync("south", 398, 100);
Task<int> east = QuoteAsync("east", 405, 200);
int[] prices = await Task.WhenAll(north, south, east);
Console.WriteLine($"WhenAll gave [{string.Join(", ", prices)}]: north, south, east, the order they were passed");
Console.WriteLine($"cheapest: {prices.Min()}");
// The same with a list of tasks built in a loop: ToList() starts every call before WhenAll waits.
string[] vendors = ["north", "south", "east"];
List<Task<int>> calls = vendors.Select((v, i) => QuoteAsync(v, 400 + i, 100 * (3 - i))).ToList();
int[] fromList = await Task.WhenAll(calls);
Console.WriteLine($"from a list of {calls.Count} tasks: [{string.Join(", ", fromList)}]");
static async Task<int> QuoteAsync(string vendor, int price, int milliseconds)
{
await Task.Delay(milliseconds);
Console.WriteLine($" {vendor} answered after {milliseconds} ms");
return price;
} Output
south answered after 100 ms east answered after 200 ms north answered after 300 ms WhenAll gave [412, 398, 405]: north, south, east, the order they were passed cheapest: 398 east answered after 100 ms south answered after 200 ms north answered after 300 ms from a list of 3 tasks: [400, 401, 402]
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run whenall.cs
The answers arrived south, east, north, after 100, 200 and 300 ms, but WhenAll gave [412, 398, 405], the order of
north, south and east as they were passed. WhenAll returns one task that completes when every task given to it has
completed, and for Task<int> tasks its result is an int[] in the order of the arguments, so the code can match
each price to its vendor by position. The whole wait took as long as the slowest call.
The second half builds the tasks with LINQ. ToList() runs the Select at once, so every call has started before
WhenAll waits; Microsoft’s guide to async scenarios asks for ToList() or ToArray() here, because a lazy query
would start nothing until something read it. When a task fails, WhenAll still waits for the others, its task ends
Faulted with every exception, and await throws the first of them.
Checkpoint
Check: WhenAll and the order of its results
WhenAny returns the first task to finish, WhenEach every task as it finishes
three_ways.cs reads three waits of 300, 100 and 200 ms with each way in turn. Each task’s result is its own delay:
// Three tasks that finish after 300, 100 and 200 ms, read three ways. Each task's result is its own delay.
Task<int>[] Start() => [Wait(300), Wait(100), Wait(200)];
// WhenAny: the first task to finish, as a task. Await it again to get its result (or its exception).
Task<int> first = await Task.WhenAny(Start());
Console.WriteLine($"WhenAny: the first task to finish waited {await first} ms");
// WhenEach (.NET 9): every task, in the order they finish, as an async stream.
await foreach (Task<int> done in Task.WhenEach(Start()))
{
Console.WriteLine($"WhenEach: a task finished, it waited {await done} ms");
}
// WhenAll: every result, in the order the tasks were passed.
int[] all = await Task.WhenAll(Start());
Console.WriteLine($"WhenAll: [{string.Join(", ", all)}], in the order they were passed");
static async Task<int> Wait(int milliseconds)
{
await Task.Delay(milliseconds);
return milliseconds;
} Output
WhenAny: the first task to finish waited 100 ms WhenEach: a task finished, it waited 100 ms WhenEach: a task finished, it waited 200 ms WhenEach: a task finished, it waited 300 ms WhenAll: [300, 100, 200], in the order they were passed
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run three_ways.cs
WhenAny completed as soon as the 100 ms task did, and its result is that task itself, which the code awaited again to
get 100. WhenEach, new in .NET 9, gave every task in the order it finished, 100, 200, 300, as an
async stream: A sequence whose items arrive asynchronously, typed IAsyncEnumerable<T>. An async iterator produces it with yield return, and await foreach reads it one item at a time. More in “Async streams with IAsyncEnumerable” read with await foreach, so each result could be used the moment it
existed. WhenAll gave [300, 100, 200], the order of the arguments. Step through the 300 ms:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- 0 ms, all three start. Three waits start at once: 300, 100 and 200 ms, passed in the order A, B, C. three_ways.cs starts a fresh set for each way of reading them; here the three ways watch one set.
- 100 ms, B finishes. Task B completes first. WhenAny completes at once, with B as its result; awaiting B gives 100.
- 100 ms, WhenEach yields B. WhenEach hands B to its await foreach loop, which prints "it waited 100 ms". WhenAll keeps waiting.
- 200 ms, C finishes. Task C completes, and WhenEach yields it next, in the order the tasks finish, not the order they were passed.
- 300 ms, A finishes. Task A completes last. WhenEach yields it and its loop ends; WhenAll, which waited for every task, completes now.
- WhenAll's result. WhenAll gives [300, 100, 200]: every result, in the order the tasks were passed, whatever order they finished in.
The four ways to wait for tasks, side by side:
| Method | Completes when | Gives you |
|---|---|---|
Task.WhenAll |
every task has completed | every result, in the order passed |
Task.WhenAny |
the first task has completed | that task, which may have failed |
Task.WhenEach |
each task completes, one by one | each task, as it finishes |
task.WaitAsync(limit) |
the task completes, or the limit passes | its result, or a TimeoutException |
WaitAsync, added in .NET 6, limits how long one wait may take. It ends the waiting when the limit passes, while the
task it waited for runs on to its own end. Before .NET 6 the same limit was written as a race between the task and a
Task.Delay, read with WhenAny. A limit that must stop the work itself is given to the work, as a cancellation
token.
Choose by when you need the results
All of them before you go on: WhenAll. Only the fastest: WhenAny. Each one as soon as it exists, to show progress or to stop early: WhenEach.
Checkpoint
Check: WhenAny and WhenEach
Failures in WhenAll and WhenAny, reproduced and handled Pro: opens with a Pro, Premium or Ultimate pass
A combined task fails differently from a single one: WhenAll hides all but one exception behind its await, and WhenAny hands back a failed winner without a word. This part reproduces both and the code that handles each.
In this part:
- Every exception of a WhenAll, and the results that survived
- A failed winner: WhenAny does not throw, the winner does
1 runnable example (C#) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Timeouts with WaitAsync, measured, and the work that keeps running Pro: opens with a Pro, Premium or Ultimate pass
A timeout on a wait is not a timeout on the work. This part measures what WaitAsync does at its limit, compares it with the older race against Task.Delay, and shows the fast path for results that are already there.
In this part:
- A 200 ms limit on a 600 ms task, measured
- The race against Task.Delay, and what it costs
- Results that are already there: Task.FromResult
1 runnable example (C#) · about 4 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
A lab: the first 10 seats from five zones, within a time limit Pro: opens with a Pro, Premium or Ultimate pass
Answers that arrive one at a time, an early stop and a time limit together are the shape of many real searches: a fare search, a stock check across warehouses, a lookup across mirrors. This lab builds one, with five checks.
In this part:
- The rules and the five checks
- Two ways to build it
1 runnable example (C#) · about 4 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Practise
The self-check covers the first two sections: the order WhenAll keeps, what WhenAny gives back, and the order WhenEach follows. It begins with the method your answers so far show you know least, and gives the reason behind every choice, right or wrong:
Practice that adapts to you
Practise this lesson
4 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · What does Task.WhenAll return, and in what order are its results?
Task.WhenAll returns one task that completes when every task passed to it has completed. For tasks that return
values, Task<T>, the combined task’s result is a T[] holding each task’s result in the order the tasks were passed,
not the order they finished: three quotes that finished south, east, north still come back as north, south, east. If
any task fails, the combined task ends faulted after all of them have completed, and awaiting it throws the first
exception. WhenAll starts nothing itself: the tasks were already running, so the total time is that of the slowest.
-
Mid to senior
An order service runs its inventory, pricing and fraud checks with await Task.WhenAll(...). During an incident two of the three fail, the logs show only one error, and support cannot explain why some orders were half processed. What is happening, and how should the code report and handle the failures?
-
Mid to senior
Product images are served by three mirrors with different speeds, and the fastest one sometimes answers with an error. You want the first good image. Write the logic with WhenAny, say what happens to the slower requests, and how you would stop wasting them.
-
Senior to staff
A team added .WaitAsync(TimeSpan.FromSeconds(2)) to a slow report query. The endpoint now answers quickly when the query is slow, but database load went up during the next peak. Explain why, and what a correct timeout for this endpoint looks like.
Key takeaways
Task.WhenAllcompletes when every task has completed and gives their results in the order they were passed; the total time is the slowest task’s.Task.WhenAnycompletes with the first task to finish, which may be a failed one, so its result is awaited again.Task.WhenEach, new in .NET 9, gives every task as it finishes, as an async stream forawait foreach.WaitAsynclimits how long a wait lasts, while the task it waits for runs on to its own end.
References
- Process asynchronous tasks as they complete (Microsoft Learn)
- Task.WhenAll method (Microsoft Learn)
- Task.WhenAny method (Microsoft Learn)
- Task.WhenEach method (Microsoft Learn)
- Task.WaitAsync method (Microsoft Learn)
- Consuming the Task-based Asynchronous Pattern (Microsoft Learn)
- Task exception handling (Microsoft Learn)
- Asynchronous programming scenarios (Microsoft Learn)
- Async return types (Microsoft Learn)
- CancellationTokenSource class (Microsoft Learn)
Report a problem with this lesson
Kept only in this browser. Your Learn progress