CodeGym /Cours /C# SELF /Gestion des exceptions dans le code asynchrone

Gestion des exceptions dans le code asynchrone

C# SELF
Niveau 59 , Leçon 4
Disponible

1. Méthodes asynchrones et exceptions

Nous avons l'habitude que si quelque chose de mauvais se passe dans le code (par exemple division par zéro ou tentative d'accès à un fichier inexistant), une exception est levée, que nous pouvons attraper avec try-catch. Tout est simple tant que le code s'exécute de façon séquentielle et dans un seul thread. Mais dès qu'apparaît l'asynchronicité, le monde ressemble à l'espace infini : l'exception peut "apparaître" loin de l'endroit où on l'attendait, ou rester complètement inaperçue.

La raison est que la méthode asynchrone retourne souvent une tâche (Task), dont l'exécution continue APRÈS la sortie de la méthode. L'exception peut survenir après que le thread principal a "relâché" l'exécution et continue sa vie. Donc la construction habituelle try-catch autour d'un appel asynchrone ne fonctionne pas toujours comme en code synchrone.

Voyons un exemple simple. Supposons que dans notre mini-application il y ait une méthode asynchrone comme celle-ci :

// Fragement de notre application : calcul asynchrone "envoi du rapport"
public async Task SendReportAsync()
{
    // Ici il pourrait y avoir des appels réseau ou un accès aux fichiers
    await Task.Delay(100);
    throw new InvalidOperationException("Erreur lors de l'envoi du rapport !");
}

Et voici comment on peut l'appeler :

SendReportAsync();
Console.WriteLine("On continue de travailler...");

Visualisation

flowchart TD
    Start["Thread principal"]
    Call[/"Appel SendReportAsync()"/]
    Continue["Le travail continue..."]
    Exception["Exception survient dans la Task"]
    Unhandled["Erreur non traitée !"]
    Start --> Call --> Continue
    Call -.- Exception --> Unhandled

Conclusion : si la méthode asynchrone retourne une Task et que vous n'attendez pas la fin de la tâche (await ou .Wait()), l'exception restera "inaperçue". Au mieux, le runtime écrira dans les logs un truc du genre "Exception non gérée dans la tâche". Au pire — vous perdrez l'erreur et passerez du temps à chercher la cause de bugs "mystérieux".

2. Comment attraper correctement les exceptions dans le code asynchrone ?

Utilisez await + try-catch

Voyons la manière correcte :

try
{
    await SendReportAsync(); // On attend la Task
    Console.WriteLine("Le rapport a été envoyé avec succès !");
}
catch (Exception ex)
{
    Console.WriteLine($"Oups ! Quelque chose a foiré : {ex.Message}");
}

Comment ça marche ? Quand vous mettez await devant l'appel asynchrone, C# découpe votre méthode en deux parties : avant et après le await. Si une exception survient dans la partie asynchrone, elle "surgit" à l'endroit où se trouve le await, et peut être interceptée par le classique try-catch.

Exemple pour l'application

Ajoutons la gestion d'erreur d'envoi du rapport dans notre démo :

public async Task StartReportProcessAsync()
{
    try
    {
        await SendReportAsync();
        Console.WriteLine("Le rapport a été envoyé avec succès !");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Erreur lors de l'envoi du rapport : {ex.Message}");
    }
}

Et appeler :

await StartReportProcessAsync();

.Wait(), .Result — pas idéal, mais utilisable en console

Parfois, surtout dans les applications console, vous ne pouvez pas utiliser await au niveau supérieur (anciennes versions de C#, méthode Main). Alors vous devez attendre la tâche de façon synchrone avec .Wait() ou .Result.

try
{
    SendReportAsync().Wait();
}
catch (AggregateException aggEx)
{
    foreach (var ex in aggEx.InnerExceptions)
        Console.WriteLine($"Erreur : {ex.Message}");
}

Pourquoi ? Les appels .Wait() et .Result enveloppent toujours l'exception d'origine dans une AggregateException. C'est un conteneur qui peut contenir une ou plusieurs exceptions. Il peut y avoir une (ou plusieurs !) exceptions internes, donc il faut les parcourir en boucle. Plus de détails sur AggregateException dans la documentation officielle.

Important !

Dans les versions modernes de .NET (à partir de C# 7.1) vous pouvez déclarer un Main asynchrone et utiliser await directement à l'entrée :

static async Task Main(string[] args)
{
    await StartReportProcessAsync();
}

3. Exceptions dans les tâches "fire-and-forget"

Que se passe-t-il si vous lancez une méthode asynchrone sans attendre sa fin et sans garder la référence de la Task ?

SendReportAsync(); // "On a oublié" la Task

Dans ce cas il y a un problème : l'exception survenue dans la Task ne sera traitée par personne. Parfois (selon l'environnement et les paramètres) l'application peut même crash. Parfois il n'y a qu'un warning dans les logs. Ce n'est pas un bug de C#, mais le résultat de la logique de fonctionnement des Tasks.

Comment faire correctement ?

  • Idéalement : n'utilisez jamais le "fire-and-forget" si vous n'êtes pas sûr que la tâche ne peut pas échouer de façon critique.
  • Si la méthode asynchrone doit vraiment être en "fire-and-forget", gérez explicitement les erreurs à l'intérieur de la méthode.
public async Task SendReportSafeAsync()
{
    try
    {
        await Task.Delay(100);
        throw new InvalidOperationException("Erreur lors de l'envoi !");
    }
    catch (Exception ex)
    {
        // On log ou on gère l'erreur
        Console.WriteLine($"[Log] Exception : {ex.Message}");
    }
}

// Appel
SendReportSafeAsync();

Recommandation générale : si la tâche n'est suivie par personne et que vous n'utilisez pas await, emballez obligatoirement le corps de la méthode asynchrone dans un try-catch. Ainsi vous ne perdrez pas l'erreur et pourrez au moins la logger.

4. Exceptions et tâches parallèles : Task.WhenAll et compagnie

Souvent dans les applications réelles il faut lancer plusieurs tâches asynchrones indépendantes et attendre leur achèvement. Par exemple, quand vous envoyez des rapports à plusieurs destinataires en parallèle :

var tasks = new List<Task>
{
    SendReportAsync(),
    SendReportAsync(),
    SendReportAsync()
};

await Task.WhenAll(tasks);

Que se passe-t-il si une (ou plusieurs) tâches lèvent une exception ?

Comment attraper ces erreurs ?

Quand on utilise await sur Task.WhenAll(tasks) — si au moins une tâche s'est terminée en erreur, await relancera l'exception de la première tâche qui a échoué (elle ne sera pas enveloppée dans une AggregateException).
Mais nuance : si plusieurs tâches ont échoué, alors une AggregateException contenant les exceptions internes sera levée.

try
{
    await Task.WhenAll(tasks);
}
catch (Exception ex)
{
    // Si c'est une AggregateException — on la démonte
    if (ex is AggregateException agg)
    {
        foreach (var inner in agg.InnerExceptions)
            Console.WriteLine($"Erreur dans la tâche : {inner.Message}");
    }
    else
    {
        Console.WriteLine($"Erreur : {ex.Message}");
    }
}

Pour un await sur une tâche unique, l'exception n'est généralement pas enveloppée dans une AggregateException. Mais avec WhenAll — ça peut arriver !

5. Délégués asynchrones et gestion des erreurs

Dans les applications UI (WPF, WinForms, ASP.NET) les handlers d'événements sont souvent écrits comme des lambdas asynchrones. Si une exception dans un tel handler "sort", le comportement dépend du framework UI : l'application peut crasher ou avaler l'erreur.

Recommandation

Utilisez toujours un try-catch à l'intérieur des délégués asynchrones :

button.Click += async (sender, args) =>
{
    try
    {
        await SendReportAsync();
    }
    catch (Exception ex)
    {
        MessageBox.Show($"Erreur : {ex.Message}");
    }
};
1
Étude/Quiz
Programmation asynchrone, niveau 59, leçon 4
Indisponible
Programmation asynchrone
Asynchronicité vs. Multithreading
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION