Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 20 minutes
  • Examples run with .NET 10.0.401
  • By MySmartCoPilot
Free core, Pro depth: 7 of 20 minutes free
Free: WhenAll waits for every task and keeps the order you gave · WhenAny returns the first task to finish, WhenEach every task as it finishes. With a pass: 3 in-depth parts and 3 interview answers, about 13 minutes.

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:

whenall.cs: three quotes, combined C# · whenall/whenall.cs
// 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

  1. Question 1 of 2 whenall.cs passes north, south and east to Task.WhenAll, and they answer in the order south, east, north. In what order are the prices in the result?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: north, south, east: the order the tasks were passed

    The result array of WhenAll matches the order of its arguments, which lets the code pair each result with the call that produced it.

  2. Question 2 of 2 Four tasks that take 120, 80, 200 and 150 ms are all started, then awaited with one await Task.WhenAll(...). About how many milliseconds pass before the await finishes?

    Type a number.

    A hint

    The tasks are already running when WhenAll is called. Which one decides when the last result is in?

    Show the answer: Question 2 of 2

    Answer: 200 ms

    WhenAll completes when the last task does, so it takes as long as the slowest task, 200 ms, not the sum of 550 ms.

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_ways.cs: WhenAny, WhenEach and WhenAll on the same waits C# · three-ways/three_ways.cs
// 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:

Interactive · step by step Three tasks read three ways, from 0 to 300 ms The tasks are on the left, in the order they were passed; the three ways to read them are on the right. Step through time.
Three tasks read three ways, from 0 to 300 ms, step 6 of 6. Showing task A: 300 ms (finished: 300), task B: 100 ms (finished: 100), task C: 200 ms (finished: 200), Task.WhenAny (done: task B), Task.WhenEach (yielded: B, C, A), Task.WhenAll ([300, 100, 200]). In focus: Task.WhenAll.firstlasttask A: 300 msfinished: 300task B: 100 msfinished: 100task C: 200 msfinished: 200Task.WhenAnydone: task BTask.WhenEachyielded: B, C, ATask.WhenAll[300, 100, 200]all done

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. 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.
  2. 100 ms, B finishes. Task B completes first. WhenAny completes at once, with B as its result; awaiting B gives 100.
  3. 100 ms, WhenEach yields B. WhenEach hands B to its await foreach loop, which prints "it waited 100 ms". WhenAll keeps waiting.
  4. 200 ms, C finishes. Task C completes, and WhenEach yields it next, in the order the tasks finish, not the order they were passed.
  5. 300 ms, A finishes. Task A completes last. WhenEach yields it and its loop ends; WhenAll, which waited for every task, completes now.
  6. 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

  1. Question 1 of 2 In three_ways.cs, what is the result of await Task.WhenAny(Start())?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The task that finished first, which the code awaits again to get 100

    await Task.WhenAny(...) gives the first task to finish. Await that task, or read its Result, to get its value or its exception.

  2. Question 2 of 2 Tasks that wait 300, 100 and 200 ms are passed to Task.WhenEach. In what order does the await foreach loop receive them?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 100, 200, 300: the order they finish

    WhenEach turns a set of tasks into an async stream in completion order, so the loop can use each result at once.

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.

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.

  1. Question 1 of 4 Tasks for ten user ids are built with ids.Select(id => GetUserAsync(id)) and passed to WhenAll without ToList(). When do the ten calls start?

    Choose one answer.

    Show the answer: Question 1 of 4

    Answer: When WhenAll enumerates the query, which it does at once, so all ten start before the wait

    The danger of a lazy query of tasks is enumerating it twice, which starts every call twice. Materialise it once with ToList() or ToArray() and pass the list.

  2. Question 2 of 4 Which code gives the same prices as await Task.WhenAll(a, b, c) for three running tasks a, b and c?

    Choose one answer.

    Show the answer: Question 2 of 4

    Answer: int[] p = [await a, await b, await c];

    With running tasks, sequential awaits and WhenAll take the same time; WhenAll adds the combined task, which keeps every exception, and reads better for a list.

  3. Question 3 of 4 A page must show each of 20 search results as soon as it arrives. Which method fits best?

    Choose one answer.

    Show the answer: Question 3 of 4

    Answer: Task.WhenEach, reading each task with await foreach as it completes

    WhenEach, added in .NET 9, hands over every task in the order it finishes, so each result can be shown the moment it exists, while WhenAll waits for the slowest.

  4. Question 4 of 4 Why does three_ways.cs write await first after Task<int> first = await Task.WhenAny(...)?

    Choose one answer.

    Show the answer: Question 4 of 4

    Answer: The first await gives the winning task; the second gets its int, or throws its exception

    Read WhenAny in two steps: which task finished first, then what that task produced.

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.

  1. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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.

    Model answer, Pro. First step and follow-ups

  3. 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.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Task.WhenAll completes when every task has completed and gives their results in the order they were passed; the total time is the slowest task’s.
  • Task.WhenAny completes 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 for await foreach.
  • WaitAsync limits how long a wait lasts, while the task it waits for runs on to its own end.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.