15 minlesson

Task.WhenAny - First Completion

Task.WhenAny - First Completion

Task.WhenAny returns when any one of the provided tasks completes. It's useful for timeouts, cancellation, and racing operations.

Basic Usage

csharp
1Task<string> task1 = FetchFromServer1Async(); // Takes 2 seconds
2Task<string> task2 = FetchFromServer2Async(); // Takes 1 second
3Task<string> task3 = FetchFromServer3Async(); // Takes 3 seconds
4
5// Returns when ANY task completes
6Task<string> firstCompleted = await Task.WhenAny(task1, task2, task3);
7
8// firstCompleted is task2 (fastest)
9string result = await firstCompleted;
10Console.WriteLine($"First result: {result}");

Important: WhenAny Returns the Task, Not the Result

csharp
1// WhenAny returns Task<Task<T>>, not Task<T>
2Task<Task<int>> whenAnyTask = Task.WhenAny(task1, task2, task3);
3Task<int> completedTask = await whenAnyTask;
4int result = await completedTask; // Get actual result
5
6// Or simplified:
7Task<int> first = await Task.WhenAny(task1, task2, task3);
8int result = await first;

Timeout Pattern

A common use case is implementing timeouts:

csharp
1async Task<string> FetchWithTimeoutAsync(string url, TimeSpan timeout)
2{
3 Task<string> fetchTask = httpClient.GetStringAsync(url);
4 Task delayTask = Task.Delay(timeout);
5
6 Task completedTask = await Task.WhenAny(fetchTask, delayTask);
7
8 if (completedTask == delayTask)
9 {
10 throw new TimeoutException($"Request to {url} timed out");
11 }
12
13 return await fetchTask;
14}
15
16// Usage
17try
18{
19 string data = await FetchWithTimeoutAsync("https://api.example.com",
20 TimeSpan.FromSeconds(5));
21}
22catch (TimeoutException)
23{
24 Console.WriteLine("The request took too long");
25}

First Successful Response

Get the first successful result, ignoring failures:

csharp
1async Task<string> FetchFromAnyServerAsync(string[] serverUrls)
2{
3 var tasks = serverUrls.Select(url => FetchAsync(url)).ToList();
4
5 while (tasks.Count > 0)
6 {
7 Task<string> completed = await Task.WhenAny(tasks);
8 tasks.Remove(completed);
9
10 try
11 {
12 return await completed; // Return first success
13 }
14 catch
15 {
16 // This server failed, try next
17 if (tasks.Count == 0)
18 throw; // All failed
19 }
20 }
21
22 throw new InvalidOperationException("No servers available");
23}

Progress Reporting

Process tasks as they complete:

csharp
1async Task ProcessAsCompletedAsync(List<Task<int>> tasks)
2{
3 var remaining = new List<Task<int>>(tasks);
4
5 while (remaining.Count > 0)
6 {
7 Task<int> completed = await Task.WhenAny(remaining);
8 remaining.Remove(completed);
9
10 try
11 {
12 int result = await completed;
13 Console.WriteLine($"Completed: {result}, Remaining: {remaining.Count}");
14 }
15 catch (Exception ex)
16 {
17 Console.WriteLine($"Failed: {ex.Message}, Remaining: {remaining.Count}");
18 }
19 }
20}

Racing Multiple Implementations

Choose the fastest implementation:

csharp
1async Task<SearchResults> SearchAsync(string query)
2{
3 var googleTask = SearchGoogleAsync(query);
4 var bingTask = SearchBingAsync(query);
5 var duckTask = SearchDuckDuckGoAsync(query);
6
7 // Return whichever responds first
8 var winner = await Task.WhenAny(googleTask, bingTask, duckTask);
9 return await winner;
10}

Cancellation with WhenAny

Cancel remaining tasks when one completes:

csharp
1async Task<string> FetchFirstWithCancellationAsync(string[] urls)
2{
3 using var cts = new CancellationTokenSource();
4 var token = cts.Token;
5
6 var tasks = urls.Select(url =>
7 FetchAsync(url, token)).ToList();
8
9 var first = await Task.WhenAny(tasks);
10
11 // Cancel remaining tasks
12 cts.Cancel();
13
14 return await first;
15}

Comparison: WhenAny vs WhenAll

WhenAnyWhenAll
Completes when first task finishesCompletes when all tasks finish
Returns the completed taskReturns array of results
Good for timeouts, racingGood for parallel execution
Other tasks continue runningAll tasks must complete

WhenAny Doesn't Cancel Other Tasks

Important: When one task completes, others keep running:

csharp
1var task1 = LongRunningAsync(); // Takes 10 seconds
2var task2 = QuickAsync(); // Takes 1 second
3
4var first = await Task.WhenAny(task1, task2);
5// task1 is STILL RUNNING in the background!
6
7// If you don't want this, use cancellation tokens

Implementing Retry with Delay

csharp
1async Task<T> RetryWithDelayAsync<T>(Func<Task<T>> operation,
2 int maxRetries,
3 TimeSpan delay)
4{
5 for (int i = 0; i < maxRetries; i++)
6 {
7 try
8 {
9 return await operation();
10 }
11 catch when (i < maxRetries - 1)
12 {
13 await Task.Delay(delay);
14 }
15 }
16
17 return await operation(); // Last attempt, let exception propagate
18}

Key Takeaways

  • WhenAny completes when the first task finishes
  • Returns Task<Task<T>> - need to await twice to get result
  • Perfect for timeout patterns with Task.Delay
  • Other tasks continue running after WhenAny returns
  • Use cancellation tokens to stop remaining tasks
  • Useful for racing, progress reporting, and fallbacks