Start independent operations before awaiting the group
Two operations are independent when neither needs the other’s result to begin. The sample calls ValueAsync twice, retaining both tasks, then awaits a single task representing their combined completion. Concurrency in the workflow means both operations can be in progress together; it does not prove that their code uses separate processor cores. The distinction is especially important in a browser-hosted runtime. Begin with the dependency graph: these two values have no data dependency, so sequentially waiting before even starting the second would impose an unnecessary ordering requirement.
Result order follows the supplied task order
The first task will return two and the second will return five. Task.WhenAll receives first and second in that order, and its successful result array preserves that input association. Joining the array therefore prints 2,5. This output is not evidence that the first timer completed before the second. Even if completion order differed, the array positions would still correspond to the task order supplied to WhenAll. Trace result identity separately from the runtime’s incidental order of timer callbacks.
var values = await Task.WhenAll(first, second);WhenAll represents completion rather than starting the methods
The ValueAsync calls already invoke their methods and return task handles before WhenAll receives them. WhenAll combines those handles; it does not retroactively turn the calls into a different dependency structure or require a new thread for each task. The combined task completes only after the supplied tasks have reached terminal states. On successful generic completion it carries their results, allowing the caller to proceed with a clearly owned collection rather than independently abandoning operations after starting them.
A failed group does not produce an ordinary successful array
If a supplied task faults, awaiting the combined task can throw instead of returning the result array. WhenAll does not automatically cancel its sibling operations because one failed. A workflow that wants early cooperative cancellation must own and signal an appropriate token and still observe the operations it started. For detailed diagnostics, keep the combined task or per-operation information rather than assuming the single exception surfaced by await describes every failure that occurred across the group.
Practice: separate output position from completion order
Reverse the arguments to WhenAll while keeping the two task variables and their returned values unchanged. Predict 5,2, because the association with supplied tasks changed. Then restore the original argument order and make the timer arguments intentionally different, without using elapsed time as a correctness assertion. The expected value order remains two then five. This experiment makes a strong contract observable while avoiding a fragile claim that small timer delays guarantee a particular physical completion order on every run.
- Label task handles by the operation they represent, not by a guessed finish order.
- Do not replace the final result association with a race winner unless that is the actual requirement.
Practice: introduce a real dependency
As a separate redesign, make the second requested value depend on the result of the first, such as adding three to it before starting the second operation. Explain why the original start-both structure no longer expresses that data dependency. Then restore independent tasks and simulate one failure with a handler around the group await. Verify that a failure message replaces the normal joined result while every started operation still has an owner. These are distinct changes to dependency and outcome, not merely syntax variations.
- Draw one graph for independent values and another with an arrow from the first result to the second request.
- Test group failure explicitly instead of treating a successful two-value demonstration as complete async error coverage.
Account for every started operation
WhenAll is useful because it gives an owner one completion boundary for a known set of tasks. Correctness still depends on starting the intended work, preserving result identity, and handling every terminal outcome. The sample’s tiny delays demonstrate managed asynchronous composition without network dependencies; they are not a throughput benchmark or a recommendation to use timers for useful work. In a larger batch, also define cancellation ownership and practical concurrency limits rather than assuming that starting every possible operation at once is always appropriate.
- Baseline output is
2,5, determined by supplied task order. - The method calls start their work; WhenAll observes the group they returned.
- Group failure, sibling lifetime, and concurrency limits require explicit workflow policies.
A working example
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
var first = ValueAsync(2);
var second = ValueAsync(5);
var values = await Task.WhenAll(first, second);
Console.WriteLine(string.Join(",", values));
async Task<int> ValueAsync(int value) { await Task.Delay(1); return value; }
Expected output
2,5
WhenAll returns successful results in the order of its input tasks.