CodeGym /Cours /C# SELF /Stratégie récapitulative de gestion des erreurs : Task, ...

Stratégie récapitulative de gestion des erreurs : Task, async/ await, AggregateException

C# SELF
Niveau 61, Leçon 4
Disponible

1. Introduction

Écrire des programmes qui ne plantent jamais, c'est un peu croire que les bugs ont peur de votre IDE. En réalité, plus le code devient complexe (surtout en environnement multithread et asynchrone), plus les bugs deviennent inventifs et les erreurs raffinées. Ignorer les exceptions dans les threads et les tasks peut facilement provoquer des fuites de ressources, des blocages, une perte de données ou des plantages inattendus des heures après le démarrage.

Dans cette leçon nous allons rassembler les meilleures pratiques de gestion des erreurs en programmation multithread et asynchrone en C#. Vous apprendrez comment attraper correctement les exceptions, comment analyser une "pile" d'erreurs concurrentes (à la manière de AggregateException), que faire des tâches lancées "pour rien", et pourquoi ignorer les exceptions est dangereux et inutile.

Pourquoi ce n'est pas si simple

  • Le thread où survient l'erreur peut être différent du thread où se trouve le try-catch.
  • Les tâches asynchrones ne lèvent pas immédiatement les exceptions — elles sont "emballées" et attendent d'être traitées lors de l'await (ou d'une attente synchrone).
  • Quand plusieurs tâches fonctionnent en parallèle (par ex. Task.WhenAll), plusieurs erreurs peuvent apparaître — il faut en tenir compte.
  • Les opérations de type fire-and-forget peuvent complètement "perdre" une exception sans gestionnaire explicite.

Cette caractéristique est une séparation du contexte d'exécution. Imaginez le programme comme un cirque avec plusieurs arènes : si un feu démarre dans l'une, il n'est pas immédiatement visible dans les autres. Il est important de savoir suivre et éteindre correctement ces "feux".

2. Exceptions dans les tâches : Task et Task<TResult>

Comment les tâches signalent les erreurs

Quand une exception non gérée survient dans une tâche, elle ne "s'échappe" pas immédiatement. La tâche devient Faulted, et l'exception est conservée à l'intérieur. On peut l'obtenir :

  • En attendant la fin avec await (ou via task.Wait()/task.Result — mais mieux vaut éviter) ;
  • En vérifiant la propriété task.Exception — elle contiendra un AggregateException.

Exemple


// Méthode asynchrone avec erreur
async Task FailAsync()
{
    await Task.Delay(100);
    throw new InvalidOperationException("Quelque chose s'est mal passé");
}

async Task MainAsync()
{
    try
    {
        await FailAsync();
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Erreur : {ex.Message}");
    }
}

Si on ne met pas de try-catch, le programme va planter. Si on en met un — l'exception sera correctement capturée, même si l'erreur est survenue dans une autre tâche.

AggregateException : erreurs en lots

Lors d'un await de Task.WhenAll(tasks) les erreurs de plusieurs tâches sont regroupées dans un seul AggregateException (dans son InnerExceptions).


async Task MultiFailAsync()
{
    Task t1 = Task.Run(() => throw new InvalidOperationException("Erreur 1"));
    Task t2 = Task.Run(() => throw new ArgumentException("Erreur 2"));
    try
    {
        await Task.WhenAll(t1, t2);
    }
    catch (Exception ex)
    {
        if (ex is AggregateException agg)
        {
            foreach (var e in agg.InnerExceptions)
                Console.WriteLine($"Exception : {e.Message}");
        }
        else
        {
            Console.WriteLine($"Erreur unique : {ex.Message}");
        }
    }
}

Attention : en utilisant await sur Task.WhenAll(tasks) .NET "déplie" le AggregateException, et dans le catch vous recevrez la "première" exception. La liste complète est accessible via task.Exception.InnerExceptions, si la tâche s'est terminée avec une erreur.

3. «Fire and Forget» : pourquoi on ne peut pas simplement oublier la tâche

"Start and forget" conduit souvent à des échecs cachés. Exemple :


Task.Run(() => { throw new Exception("Boum!"); }); // L'erreur "s'envole" dans le vide.

Le runtime moderne garde l'erreur dans la tâche et peut terminer le processus à cause d'une exception non observée. La meilleure pratique est de conserver une référence à la tâche et/ou de s'abonner à l'événement TaskScheduler.UnobservedTaskException.

Comment faire correctement ?

  • Conservez la tâche pour attendre sa fin et traiter l'erreur ;
  • Pour les fire‑and‑forget, placez un gestionnaire directement à l'intérieur du délégué.

Task.Run(() => {
    try 
    {
        // Code qui peut lancer une exception
    }
    catch (Exception ex)
    {
        // On loggue, on notifie, mais on ne laisse pas l'erreur s'échapper
        Console.WriteLine($"Erreur dans fire-and-forget : {ex.Message}");
    }
});

4. Erreurs dans les threads (Thread) : "rien à attraper ?"

Une exception dans un nouveau Thread ne peut pas être attrapée par un try-catch extérieur — seulement à l'intérieur du corps du thread.


var thread = new Thread(() =>
{
    try
    {
        throw new Exception("Erreur dans le thread");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Attrapé dans le thread : {ex.Message}");
    }
});
thread.Start();

Si on ne met pas de gestionnaire, l'exception ne terminera que ce thread (si c'est un thread en arrière‑plan : thread.IsBackground = true). Pour les threads non background, une exception non gérée peut terminer tout le processus. Mettez toujours un try-catch à l'intérieur du thread.

Comment retourner résultat et erreurs depuis un thread ?

  • Utilisez des queues/collections pour transférer résultats/erreurs ;
  • Modèle basé sur des événements ;
  • Mieux : passez aux tasks — elles simplifient la gestion des erreurs.

5. Boucles parallèles : on attrape les erreurs différemment

Dans les boucles parallèles les erreurs provenant de différentes branches sont agrégées dans un AggregateException.


try
{
    Parallel.For(0, 5, i =>
    {
        if (i % 2 == 0)
            throw new Exception($"Erreur à l'itération {i}");
    });
}
catch (AggregateException ex)
{
    foreach (var e in ex.InnerExceptions)
        Console.WriteLine($"[Boucle parallèle] Erreur : {e.Message}");
}

Si vous voulez logger les échecs locaux et continuer les autres branches, placez un try-catch interne dans chaque branche.

6. Gestion des erreurs lors de l'annulation des tâches

Lors d'une annulation via CancellationToken, par convention on lance OperationCanceledException — ce n'est pas une défaillance, mais un arrêt normal.


async Task DoWorkAsync(CancellationToken token)
{
    for (int i = 0; i < 10; i++)
    {
        token.ThrowIfCancellationRequested();
        await Task.Delay(100);
    }
}

// Quelque part dans le code :
var cts = new CancellationTokenSource();
var task = DoWorkAsync(cts.Token);
cts.Cancel(); // Environ après 200 ms

try
{
    await task;
}
catch (OperationCanceledException)
{
    Console.WriteLine("L'opération a été annulée !");
}

En plus de ThrowIfCancellationRequested(), beaucoup de méthodes supportant le token (par ex. Task.Delay, HttpClient.GetAsync) lanceront elles‑mêmes OperationCanceledException. Vérifiez le support de l'annulation.

7. Nuances utiles

Approches pour tous les cas

  • Mettez un try-catch au niveau supérieur des méthodes asynchrones — ainsi vous attraperez les erreurs "échappées".
  • N'ignorez pas les tâches : conservez des références, attendez la fin, logguez les erreurs.
  • Pour les opérations parallèles (Task.WhenAll / Parallel.ForEach) tenez compte de AggregateException.
  • Séparez annulation et échecs : attrapez OperationCanceledException séparément.
  • Logguez les erreurs, surtout les "silencieuses" qui ne plantent pas l'application.
  • Conservez les détails : logguez toutes les exceptions imbriquées, pas seulement la première.
  • Gérez les erreurs "sur place" : si vous ne savez pas quoi faire — au moins logguez.

Où attraper l'erreur dans le code multithread et asynchrone

flowchart TD
    A[Thread principal / UI] -->|Lance une tâche| B[Task/async]
    B -->|Dans la tâche| C[try/catch dans la méthode asynchrone]
    B -->|await dans le thread principal| D[try/catch autour de await]
    A -->|Lance un Thread| E[Thread]
    E -->|À l'intérieur| F[try/catch dans le thread]
    B -->|Beaucoup de tâches| G[Task.WhenAll / Parallel.ForEach]
    G -->|Erreur| H[AggregateException]

8. Erreurs typiques et pièges

Erreur n°1 : attendre une tâche via .Result ou .Wait(). Risque de deadlock et/ou d'un AggregateException inattendu.

Erreur n°2 : lancer un fire‑and‑forget sans try-catch interne — la tâche peut tomber silencieusement, sans diagnostic.

Erreur n°3 : erreurs manquées dans les boucles parallèles — une partie du travail n'est pas faite et vous ne le remarquez pas.

Erreur n°4 : ne pas différencier annulation (OperationCanceledException) et vraies erreurs.

Erreur n°5 : logger seulement la première erreur parmi plusieurs tâches — les autres restent "dans l'ombre".

Erreur n°6 : réutiliser le même objet Exception pour toutes les tâches — chaque erreur doit avoir sa propre instance.

Erreur n°7 : absence de gestion des exceptions dans le thread UI — une erreur en arrière‑plan passe inaperçue, l'interface se comporte "comme un fantôme".

2
Mission
C# SELF, niveau 61, leçon 4
Bloqué
Gestion des exceptions dans une tâche asynchrone
Gestion des exceptions dans une tâche asynchrone
1
Étude/Quiz
Exceptions dans les Threads classiques, niveau 61, leçon 4
Indisponible
Exceptions dans les Threads classiques
Gestion des erreurs dans le code asynchrone
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION