Async .NET · Lesson 16

WhenAny returns a winner that may have failed

Observe the winning task rather than treating race completion as operation success.

How it works

WhenAny completes when one supplied task reaches a terminal state. That state can be successful, canceled, or faulted. The outer race tells the caller which task finished; it does not promise that the winning operation produced a usable result. This sample deliberately makes a failed task the only completed candidate.

Awaiting WhenAny returns the winning Task<int>. Reading IsFaulted shows its failure state, and awaiting the winner propagates the InvalidOperationException for handling. The second await is essential to the meaning of the program. Without it, code might announce a successful race while ignoring the actual operation failure.

The pending task remains active after the race is decided. Completing it with nine and awaiting it demonstrates that WhenAny neither cancels nor discards other operations. A real workflow must decide whether to keep waiting, request cancellation, or retain references so later failures can be observed.

Do not assume the winner is the best result. A fast failure can beat a slower successful operation. Redundant-service or fallback designs may need a loop that accepts the first successful result while collecting failures from unsuccessful candidates. That policy is more specific than simply taking the first completion.

Practice by replacing the failed candidate with Task.FromResult(4). Predict the winner's state and output, then cancel a candidate with an already-canceled token. Compare how the outer race completes with how awaiting its returned task behaves for each terminal state.

Finally, sketch the ownership rule for the nonwinning task. If it represents a resource-consuming operation, who requests cancellation and who waits for cleanup? If cancellation only stops a caller's wait, the underlying work may continue. Explicit lifetime accounting makes race-based code reliable even when the fastest task is not a successful one.

A working example

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

var pending = new TaskCompletionSource<int>();
Task<int> failed = Task.FromException<int>(new InvalidOperationException("failed"));
Task<int> winner = await Task.WhenAny(pending.Task, failed);
Console.WriteLine(winner.IsFaulted);
try { await winner; }
catch (InvalidOperationException) { Console.WriteLine("observed"); }
pending.SetResult(9);
Console.WriteLine(await pending.Task);

Expected output

True
observed
9
Try this
Await WhenAny to find the completed task, then await that task to observe its outcome.