CodeGym /Cours /C# SELF /Plusieurs exceptions :

Plusieurs exceptions : AggregateException

C# SELF
Niveau 61 , Leçon 1
Disponible

1. Introduction

Imaginez : vous lancez une dizaine de tâches asynchrones en même temps — parser des sites, vérifier des prix sur des marketplaces ou calculer de grands tableaux de données. Tout démarre bien, mais plusieurs tâches plantent avec des erreurs : un serveur ne répond pas ici, un problème de données là. Dans du code synchrone vous verriez une seule exception, alors qu'en asynchrone il peut y en avoir plusieurs. C'est là qu'intervient AggregateException, qui regroupe toutes les erreurs dans un seul "conteneur".

Qu'est-ce que AggregateException ?

AggregateException — un type d'exception spécial dans .NET qui agrège les erreurs des opérations parallèles/asynchrones : Task.WhenAll, Parallel.For, TPL, etc. Quand deux tâches ou plus se terminent avec une erreur, elles sont "emballées" dans un seul objet AggregateException.

Où survient AggregateException ?

Le scénario le plus courant — attendre plusieurs tâches via Task.WhenAll :

// Exemple — on appelle plusieurs tâches en parallèle, certaines lancent une exception
var tasks = new List<Task>
{
    Task.Run(() => throw new InvalidOperationException("Erreur #1")),
    Task.Run(() => throw new ArgumentException("Erreur #2")),
    Task.Run(() => { Console.WriteLine("Tâche 3 exécutée avec succès !"); })
};

try
{
    await Task.WhenAll(tasks); // Avec await, AggregateException est déroulée ici
}
catch (Exception ex)
{
    Console.WriteLine($"Une erreur est survenue : {ex.GetType().Name} - {ex.Message}");
}

Mais il y a un détail : avec await le AggregateException est déroulée et la première exception interne est relancée "telle quelle". Si vous n'utilisez pas await, et que vous accédez à Result ou appelez Wait(), vous obtiendrez l'AggregateException elle-même.

2. À quoi ressemble AggregateException ?

C'est une exception qui contient une collection InnerExceptions — toutes les erreurs imbriquées :

try
{
    Task task = Task.WhenAll(tasks);
    task.Wait(); // Ici on aura précisément AggregateException si des erreurs sont survenues
}
catch (AggregateException aggEx)
{
    Console.WriteLine($"Nombre total d'erreurs : {aggEx.InnerExceptions.Count}");
    foreach (var e in aggEx.InnerExceptions)
    {
        Console.WriteLine($"Type : {e.GetType().Name}, Message : {e.Message}");
    }
}

Exemple de sortie :

Nombre total d'erreurs : 2
Type : InvalidOperationException, Message : Erreur #1
Type : ArgumentException, Message : Erreur #2

Comment se forment les AggregateException


+--------------------------+
| On lance plusieurs       |
|   Task                   |
+-----+---------+----------+
      |         |
      v         v
  Task1    Task2 ... TaskN
   |          |
   |  (échoue avec erreur)
   |-------------------+
   |                   v
   |              Exception2
   v
Exception1

   |
   v
+---------------------------------+
| Task.WhenAll ou Parallel.For    |
|        (agrège toutes           |
|         les exceptions)         |
+---------------------------------+
              |
              v
     AggregateException
   (InnerExceptions : toutes les erreurs)

Pourquoi on ne voit pas AggregateException avec await ?

Avec await .NET "déroule" le AggregateException et relance la première exception interne (InnerExceptions[0]).

// Une seule tâche qui lance InvalidOperationException
try
{
    await Task.Run(() => throw new InvalidOperationException("Erreur !"));
}
catch (Exception ex)
{
    // Ici ex est précisément InvalidOperationException, pas AggregateException
    Console.WriteLine(ex.GetType().Name); // InvalidOperationException
}

Si vous voulez accéder à toutes les erreurs — utilisez Wait() ou Result (mais attention au blocage de thread).

3. Attraper et traiter toutes les exceptions

Relions la théorie à la pratique. Supposons que plusieurs tâches traitent des données utilisateur, et il faut enregistrer toutes les défaillances.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace MyApp
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var tasks = new List<Task>
            {
                Task.Run(() => throw new InvalidOperationException("Mauvaise opération !")),
                Task.Run(() => throw new DivideByZeroException("Division par zéro !")),
                Task.Run(() => Console.WriteLine("La troisième tâche s'est bien passée"))
            };

            try
            {
                // On attend les tâches de façon asynchrone — on obtient la première exception interne
                Task allTasks = Task.WhenAll(tasks);
                await allTasks; // avec await AggregateException est déroulée
            }
            catch (Exception ex)
            {
                // Avec await on verra la première erreur
                Console.WriteLine($"Première erreur : {ex.GetType().Name} - {ex.Message}");

                // Si c'est un AggregateException — on peut itérer toutes les exceptions
                if (ex is AggregateException aggEx)
                {
                    foreach (var inner in aggEx.InnerExceptions)
                    {
                        Console.WriteLine($"Erreur de la liste : {inner.GetType().Name} - {inner.Message}");
                    }
                }
            }

            // Maintenant on attrape toutes les erreurs de façon synchrone
            try
            {
                var allTasks = Task.WhenAll(tasks);
                allTasks.Wait(); // bloque le thread — ne faites pas ça dans l'UI !
            }
            catch (AggregateException aggEx)
            {
                foreach (var e in aggEx.InnerExceptions)
                {
                    Console.WriteLine($"[Wait] Erreur : {e.GetType().Name} — {e.Message}");
                }
            }
        }
    }
}

Avec await dans le bloc try/catch vous verrez seulement la première erreur. Avec Wait() vous recevrez le classique AggregateException contenant toutes les erreurs imbriquées.

4. Traitement des erreurs en masse

Dans les applications réelles il faut souvent traiter plusieurs erreurs différemment. Par exemple, vous envoyez des e-mails à tous les utilisateurs, et certains envois échouent :

var sendTasks = emailList.Select(email => Task.Run(() => SendEmail(email))).ToList();

try
{
    Task.WaitAll(sendTasks.ToArray());
}
catch (AggregateException aggEx)
{
    foreach (var ex in aggEx.InnerExceptions)
    {
        if (ex is SmtpException)
            Console.WriteLine("Erreur d'envoi de l'email : " + ex.Message);
        else
            Console.WriteLine("Erreur inconnue : " + ex.Message);
    }
}

Comme ça vous enregistrez chaque échec et pouvez prendre des mesures individuelles.

5. Astuces utiles

Raisons de ce comportement

.NET privilégie la commodité : dans l'UI la première erreur suffit souvent (avec await). Mais côté serveur ou dans des systèmes où il faut gérer toutes les défaillances, AggregateException est indispensable.

Pattern : Flatten — dérouler la liste d'erreurs

La méthode Flatten() transforme les AggregateException imbriquées en une liste plate :

catch (AggregateException aggEx)
{
    foreach (var ex in aggEx.Flatten().InnerExceptions)
    {
        Console.WriteLine($"Erreur déroulée : {ex.GetType().Name} — {ex.Message}");
    }
}

Quel comportement a Task selon la façon d'attendre

Comment on attend la tâche Quelle exception on attrapera
await Task.WhenAll()
La première erreur interne
Task.WhenAll().Wait()
Toujours AggregateException
Task.WhenAll().Result
Toujours AggregateException
Await individuel L'exception lancée par la tâche, ou la première erreur interne si la tâche s'est terminée avec AggregateException

À retenir

  • AggregateException est utile dans tous les scénarios où plusieurs erreurs peuvent survenir en même temps.
  • Avec await il est pratique d'attraper la première erreur ; pour l'attente synchrone — itérer toutes les InnerExceptions.
  • Utilisez Flatten() et Handle() pour un traitement soigné.
  • Ne mélangez pas attentes synchrones et asynchrones dans l'UI : vous allez provoquer des freezes et des deadlocks.

7. Erreurs typiques lors du traitement de AggregateException

Mythe : await Task.WhenAll lance toujours AggregateException. En fait non : l'exception est déroulée et vous verrez la première interne.

Accéder à Result ou appeler Wait() bloque le thread jusqu'à la fin des tâches. Dans l'UI ça provoquera des gelures de l'interface.

Si vous voulez traiter certaines erreurs "silencieusement", utilisez Handle() :

catch (AggregateException aggEx)
{
    aggEx.Handle(ex =>
    {
        if (ex is InvalidOperationException)
        {
            // Cette erreur est traitée, on n'a pas besoin de la relancer
            return true;
        }
        return false; // Les autres exceptions seront relancées
    });
}
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION