Back to Blog
C#

C# Task.WhenAny: Handling the First Completed Task

c# task whenany: Learn how to use Task.WhenAny in C# to await the first completed task, handle timeouts, cancellation, and manage multiple async operations efficiently.

C#asyncconcurrencyTask.WhenAnyasync/awaitcancellation
Illustration of multiple asynchronous tasks racing, with the first completed one highlighted as Task.WhenAny in C#.

c# task whenany requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When you have several independent asynchronous operations and you only need the first one to finish, Task.WhenAny is the method that fits. It returns a Task<Task> that completes as soon as any of the supplied tasks completes. The returned task does not wait for all tasks; it signals the moment the first one reaches a terminal state. This is useful for timeouts, race conditions, and failover scenarios.

How Task.WhenAny Signals Completion

Task.WhenAny accepts an IEnumerable<Task> or a parameter array of tasks. It returns a Task<Task> that, when awaited, yields the first Task that completed. The completion state of that task can be RanToCompletion, Faulted, or Canceled. You must inspect the returned task to determine which one finished and what its result or exception is.

Task<int> first = Task.FromResult(42); Task<int> second = Task.Delay(1000).ContinueWith(_ => 99); Task<int> completedTask = await Task.WhenAny(first, second); int result = await completedTask; // 42

The await on WhenAny gives you the actual task that finished. A second await on that task retrieves its result or throws its exception. This two-step pattern is essential because WhenAny itself does not unwrap the inner task.

Minimal Example: Awaiting the First Completed Task

Consider a scenario where you query two redundant services and accept whichever responds first. Each service call is an async method returning HttpResponseMessage.

Task<HttpResponseMessage> callPrimary = FetchFromService("primary"); Task<HttpResponseMessage> callSecondary = FetchFromService("secondary"); Task<HttpResponseMessage> winner = await Task.WhenAny(callPrimary, callSecondary); HttpResponseMessage response = await winner;

After WhenAny completes, the losing task is still running. You are not obligated to await it, but you should consider its eventual completion to avoid unobserved exceptions. If the losing task faults later, the exception becomes unobserved unless you attach a continuation or await it later. In many cases, you will want to cancel the remaining tasks, which we cover later.

Common Pattern: Timeout with Task.WhenAny

A frequent use of c# task whenany is implementing a timeout without a dedicated CancellationToken source. Combine your operation with Task.Delay and let the first completion decide.

Task<string> operation = FetchDataAsync(); Task timeout = Task.Delay(TimeSpan.FromSeconds(5)); Task completed = await Task.WhenAny(operation, timeout); if (completed == timeout) { // Handle timeout, but note: operation is still running. throw new TimeoutException("Operation timed out."); } string result = await operation;

This pattern is simple, but it does not stop the underlying operation. If the operation holds resources like network connections or database handles, you should still cancel it. A better approach combines WhenAny with a CancellationToken that you trigger when the timeout fires.

Handling the First Success: Filtering by Result

Sometimes you want the first task that succeeds, not just the first that completes. WhenAny alone does not filter by result. You need to wrap each task so that failures are treated as non-winners. A common technique is to attach a continuation that returns a sentinel value on exception.

Task<string>[] tasks = urls.Select(url => FetchAsync(url)).ToArray(); Task<string> winner = await Task.WhenAny( tasks.Select(async task => { try { return await task; } catch { return null; // Treat failure as not a winner. } }) ); string result = await winner; if (result == null) { // All tasks failed. }

This changes the semantics: the first task to complete successfully wins. Tasks that fault early are ignored, and WhenAny waits for the next one. However, you must still handle the case where all tasks fail. A more robust approach uses Task.WhenAll and catches AggregateException, but that waits for all tasks, not the first success.

Error Handling with Task.WhenAny

When WhenAny returns a faulted task, the exception is not thrown until you await that specific task. This means you can inspect the state of the returned task before deciding how to react.

Task<int> task = Task.FromException<int>(new InvalidOperationException("Bad")); Task<int> completed = await Task.WhenAny(task); if (completed.IsFaulted) { // Handle the exception without throwing immediately. var exception = completed.Exception; } else { int result = await completed; }

If you simply await completed without checking, the exception propagates normally. For multiple tasks, you must be careful about unobserved exceptions from the tasks that did not win. Always attach a continuation or await the remaining tasks eventually, or cancel them to prevent unobserved fault exceptions.

Cancellation and Resource Cleanup

A common production scenario is to start several tasks and cancel the losers once one finishes. Task.WhenAny does not cancel anything automatically. You need a CancellationTokenSource that you cancel after the first task completes.

using var cts = new CancellationTokenSource(); Task<string> primary = FetchAsync(cts.Token); Task<string> secondary = FetchAsync(cts.Token); Task<string> winner = await Task.WhenAny(primary, secondary); cts.Cancel(); // Signal the other task to stop. string result = await winner;

Cancellation requires that the async methods honor the token. If they do not, canceling has no effect, and the tasks continue consuming resources. For operations that cannot be cancelled, you may need to accept the resource usage or use a different design, such as a single shared task.

Task.WhenAny vs Task.WhenAll

Task.WhenAll waits for all tasks to complete, while Task.WhenAny returns as soon as the first one finishes. The choice depends on whether you need the aggregate result or the earliest completion.

CriterionTask.WhenAnyTask.WhenAll
Completion conditionFirst task completesAll tasks complete
Return typeTask<Task>Task<T[]>
Exception behaviorOnly the winner's exception is immediate; others unobservedAggregates all exceptions
Typical useTimeout, race, failoverFan-out, parallel processing

Use WhenAll when you need all results and can tolerate waiting. Use WhenAny when the first response is sufficient, but be prepared to handle the remaining tasks.

Performance and Resource Considerations

Task.WhenAny itself is lightweight; it creates a continuation on each input task. The overhead is proportional to the number of tasks, but it is small compared to the actual async work. The real cost is in the tasks you start and do not await. Each unobserved task holds resources until it completes. In high-volume scenarios, starting many tasks and ignoring the losers can lead to memory pressure and thread pool saturation.

If you frequently use WhenAny for timeouts, consider whether a CancellationToken with a linked source is more efficient. The Task.Delay approach creates an extra timer per call. For short-lived operations, the overhead is negligible; for long-running or high-frequency operations, it adds up.

Another subtlety: WhenAny does not reorder tasks. The returned task is the first to reach a terminal state, but if multiple tasks complete at the same time, the selection is nondeterministic. Do not rely on order when tasks have equal completion times.

Finally, when you use WhenAny in a loop, be careful not to create a new Task.Delay for each iteration without disposing of the previous one. The timer is not disposed until the delay completes, which can accumulate if you create many timeouts quickly. Reuse a single CancellationTokenSource for timeouts when possible.

c# task whenany: Practical Usage and Code Examples | RYUSLOG DEV