Task.WhenAny vs Task.WhenEach : quand utiliser quoi en .NET 9
Task.WhenAny et Task.WhenEach ne résolvent pas le même problème. L'un aide à réagir au premier résultat disponible, l'autre simplifie le traitement progressif d'un ensemble de tâches.

La concurrence applicative en .NET devient difficile quand plusieurs tâches doivent être orchestrées avec des comportements différents: attendre la première réponse, traiter les résultats au fil de l'eau, annuler les retardataires ou agréger proprement les erreurs.
Dans ce contexte, Task.WhenAny et Task.WhenEach paraissent proches, mais ils servent des objectifs différents.
Ce que fait vraiment Task.WhenAny
Task.WhenAny retourne dès qu'une tâche est terminée parmi une collection. Cela ne signifie pas que les autres s'arrêtent. Cela signifie seulement que tu peux réagir au premier résultat disponible.
var tasks = new[]
{
FetchFromPrimaryAsync(ct),
FetchFromReplicaAsync(ct),
FetchFromCacheAsync(ct)
};
var firstCompleted = await Task.WhenAny(tasks);
var result = await firstCompleted;
Ce pattern est utile pour:
- hedging requests
- fallback rapide
- lecture depuis plusieurs sources
- arbitrage sur la première réponse exploitable
Le piège le plus classique avec WhenAny
Une erreur fréquente consiste à oublier que les autres tâches continuent de tourner. Il faut donc se poser la question de leur gestion:
- faut-il les annuler ?
- faut-il ignorer proprement leur résultat ?
- faut-il journaliser les échecs ?
Sans cela, WhenAny peut laisser vivre des traitements inutiles plus longtemps que nécessaire.
Ce que change Task.WhenEach
Task.WhenEach permet de consommer les tâches à mesure qu'elles se terminent, sans avoir à reconstruire une boucle manuelle autour de WhenAny.
await foreach (var task in Task.WhenEach(tasks))
{
var result = await task;
Console.WriteLine(result);
}
Cette approche est très utile quand:
- l'ordre final n'a pas d'importance
- chaque résultat peut être traité indépendamment
- on veut commencer le travail sans attendre la fin globale
Quand préférer WhenEach
WhenEach est plus lisible que WhenAny pour:
- pipeline de fan-out / fan-in
- chargement parallèle de données
- enrichissement progressif d'un document
- traitement par lot avec rendu partiel
Le bénéfice n'est pas seulement syntaxique. Il clarifie l'intention: traiter chaque fin de tâche, pas seulement la première.
Une comparaison simple
Task.WhenAny
- réagit à la première tâche terminée
- utile pour le racing ou le fallback
- demande souvent une gestion manuelle des autres tâches
Task.WhenEach
- expose les tâches au fil de leur complétion
- utile pour le traitement progressif
- réduit le code d'orchestration répétitif
Gestion des erreurs
Dans les deux cas, il faut rester explicite sur les exceptions.
Avec WhenEach, chaque await task peut lever une exception. Il est donc souvent préférable de traiter erreur et succès au même endroit:
await foreach (var task in Task.WhenEach(tasks))
{
try
{
var item = await task;
await ProcessAsync(item, ct);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Parallel task failed.");
}
}
Cette granularité est souvent meilleure qu'un échec global unique.
Performance et lisibilité
Le gain principal de WhenEach n'est pas une révolution de performance. C'est une amélioration de lisibilité pour des patterns fréquents d'orchestration asynchrone.
Quand le code concurrent devient lisible:
- il est plus simple à relire
- il est plus facile à tester
- il devient moins risqué à faire évoluer
En engineering, ce gain vaut souvent autant qu'une micro-optimisation.
Conclusion
Task.WhenAny et Task.WhenEach ne sont pas concurrents. Ils répondent à deux besoins distincts.
Utilise WhenAny quand seule la première complétion compte. Utilise WhenEach quand tu veux traiter un ensemble de tâches à mesure qu'elles se terminent. Le bon choix dépend de l'intention du flux, pas d'une préférence de syntaxe.

