CodeGym /Cours /C# SELF /Exceptions dans les tâches "fire and forget"

Exceptions dans les tâches "fire and forget"

C# SELF
Niveau 61, Leçon 0
Disponible

1. Qu'est-ce que "fire and forget" ?

En programmation, le terme fire and forget signifie lancer une tâche sans attendre sa fin. Dans le monde C# et .NET, on le fait souvent avec des Task : on les lance, on ne les await pas, on ne garde pas la référence et on les oublie.

// Le bouton lance une tâche en arrière-plan, mais elle n'est awaitée nulle part.
button.Click += (s, e) =>
{
    Task.Run(() => OperationLongue());
};

Ça paraît séduisant : "laisse tourner en arrière-plan, je m'occupe d'autre chose". Mais avec cette approche, si une exception survient dans la tâche, personne ne le remarquera à temps — elle se perdra silencieusement.

2. Comment fonctionne la gestion des exceptions dans Task

Classique : await et gestion des erreurs

La manière standard de travailler avec des tâches asynchrones est via await. Si une erreur survient dans la tâche, elle sera relancée au point d'attente :

try
{
    await SomeOperationAsync(); // si une Exception est levée ici, elle ira dans le catch
}
catch(Exception ex)
{
    Console.WriteLine("Oups ! Une erreur est survenue dans la tâche : " + ex.Message);
}

Autrement dit, quand vous attendez la fin de la tâche, vous ne manquerez pas l'exception.

Mais les tâches "fire and forget" ne sont attendues par personne !

public void LancerSansAttente()
{
    // La tâche vit pour elle-même. Personne ne l'attend...
    Task.Run(() => {
        // Quelque part dedans, un problème arrive :
        throw new InvalidOperationException("Oups, tout est perdu !");
    });
    // La méthode s'est terminée, la tâche tourne tranquillement en arrière-plan.
}

Si une exception survient dans une telle tâche, elle ne sera pas relancée dans le thread principal. L'application continue de fonctionner comme si de rien n'était.

Important

Dans .NET, une tâche avec une exception non gérée passe en état Faulted. Mais si vous ne l'attendez pas (await, .Result, .Wait(), etc.), personne ne lira l'exception et elle ne se manifestera pas dans le code appelant.

Que se passe-t-il réellement "sous le capot" ?

Pour les tâches que personne n'attend, il ne reste qu'une seule chance d'être remarquées — l'événement TaskScheduler.UnobservedTaskException. Il est déclenché quand le garbage collector (GC) trouve une tâche avec une exception non observée. Mais ça n'arrive pas tout de suite et pas forcément là où vous l'attendez — il ne faut pas compter dessus.

3. Démonstration : une erreur fire-and-forget

// Exemple : on lance une tâche fire-and-forget depuis Main
using System;
using System.Threading.Tasks;

class Program
{
    static void Main(string[] args)
    {
        FireAndForgetExample();

        Console.WriteLine("Le thread principal continue de fonctionner...");
        // On donne du temps à la tâche pour finir
        Task.Delay(2000).Wait();
    }

    static void FireAndForgetExample()
    {
        Task.Run(() =>
        {
            Console.WriteLine("La tâche fire-and-forget a commencé !");
            Task.Delay(500).Wait();
            throw new InvalidOperationException("Erreur à l'intérieur de la tâche fire-and-forget !");
        });
    }
}

Si vous lancez ce code, eh bien... rien de spécial ne se produira. L'erreur arrivera, mais le programme ne la remarquera pas. Parfois on voit un avertissement dans la fenêtre Output de l'IDE, mais pour l'utilisateur — aucune information.

Pourquoi c'est dangereux dans des vrais projets ?

  • Bogs difficiles à reproduire ("parfois ça marche — parfois non, aucune idée pourquoi").
  • Perte silencieuse de données ou de logique (par ex. un mail qui n'a pas été envoyé).
  • En production — absence de signaux sur les problèmes si le logging n'est pas configuré.

4. Manières correctes de gérer les erreurs en fire-and-forget

Logger et gérer les erreurs à l'intérieur de la tâche

Le minimum sûr est d'attraper les exceptions directement dans la tâche fire-and-forget :

Task.Run(() =>
{
    try
    {
        // Votre code long/dangereux
        throw new InvalidOperationException("Quelque chose s'est mal passé !");
    }
    catch (Exception ex)
    {
        // On log l'erreur ou on informe l'utilisateur
        Console.WriteLine("Fire-and-forget : exception interceptée : " + ex.Message);
        // On peut écrire dans un fichier de log, utiliser un système d'alerte, etc.
    }
});

Méthodes async void (et pourquoi c'est une mauvaise idée)

async void DangerousFireAndForget()
{
    // Quelque chose de dangereux
    throw new Exception("Boum !");
}

Les méthodes async void sont en pratique des fire-and-forget : on ne peut pas les attendre, elles ne retournent pas de Task. Les exceptions venant d'elles partent vers le gestionnaire global de l'application (par ex. AppDomain.UnhandledException) et souvent provoquent le crash du process. N'utilisez async void que pour les handlers d'événements — et encore, avec prudence.

Utiliser des helpers pour lancer en sécurité

Pratique de factoriser le lancement sûr de fire-and-forget dans un wrapper :

// Méthode universelle pour lancer un fire-and-forget en sécurité
public static void RunSafeFireAndForget(Func<Task> taskFactory)
{
    Task.Run(async () =>
    {
        try
        {
            await taskFactory();
        }
        catch (Exception ex)
        {
            // On log l'exception
            Console.WriteLine("Fire-and-forget (safe) : " + ex);
            // On peut ajouter l'envoi vers un système de monitoring !
        }
    });
}

// Utilisation :
RunSafeFireAndForget(async () =>
{
    await Task.Delay(1000);
    throw new InvalidOperationException("À l'intérieur du fire-and-forget !");
});

Exemple "réel" : envoi d'email

// Bouton d'envoi d'email :
private void buttonSend_Click(object sender, EventArgs e)
{
    Task.Run(() => SendEmail());
}

// Méthode d'envoi :
private void SendEmail()
{
    try
    {
        // Ici il pourrait y avoir un envoi réel
        throw new Exception("Le serveur SMTP est indisponible !");
    }
    catch (Exception ex)
    {
        // Logging
        File.AppendAllText("errors.log", $"Erreur d'envoi : {ex.Message}\n");
    }
}

5. Et TaskScheduler.UnobservedTaskException ?

En dernier recours, .NET expose l'événement TaskScheduler.UnobservedTaskException. Il est appelé si une tâche s'est terminée avec une erreur, que personne ne l'a attendue, et que l'objet tâche a été collecté par le GC. Il ne faut pas compter dessus — c'est un mécanisme de "dernier recours".

TaskScheduler.UnobservedTaskException += (sender, e) =>
{
    Console.WriteLine("UnobservedTaskException global : " + e.Exception);
    e.SetObserved(); // N'oublie pas d'appeler ça, sinon l'app peut planter !
};

Pour en savoir plus : TaskScheduler.UnobservedTaskException.

6. Nuances utiles

Comparaison schématique des approches

Approche Exceptions gérées ? Où attraper les erreurs Risque de "perdre" l'erreur
await
Oui Dans le code appelant Faible
Fire-and-forget sans try/catch Non Nulle part Très élevé
Fire-and-forget avec try/catch Oui À l'intérieur de la tâche elle-même Faible (si vous loggez)
async void-méthode Non (va vers le global) Gestionnaire global Élevé

Comment concevoir correctement le fire-and-forget

  • Si le résultat ou l'état de la tâche est critique — ne faites pas de fire-and-forget. Utilisez await ou conservez la Task pour l'attendre plus tard.
  • Le fire-and-forget est justifié uniquement pour des tâches vraiment non critiques (par ex. envoi de télémétrie).
  • Emballez toujours le fire-and-forget dans votre propre méthode et attrapez / loggez les exceptions.
  • Pour des scénarios d'arrière-plan complexes, utilisez des queues et workers : Hangfire, Quartz.NET.

Usage pratique et interviews

Aux entretiens on demande souvent : "Que se passe-t-il si une exception survient dans une tâche fire-and-forget ?" ou "Pourquoi on ne peut pas utiliser async void partout ?" Bonne réponse : vous êtes responsable des erreurs des tâches d'arrière-plan — soit vous les attrapez, loggez et analysez, soit vous vous retrouvez avec des bugs fantômes.

Comparaison "fire-and-forget" vs await

Scénario Fiabilité du traitement d'erreurs Applicabilité
Attendre avec await Excellente Partout où le résultat ou le succès/échec est important
Fire-and-forget Mauvaise (si non géré manuellement) Seulement pour des tâches vraiment en arrière-plan et non critiques
Fire-and-forget avec try/catch Bonne (si vous loggez) Tâches d'arrière-plan où le résultat n'est pas nécessaire, mais où les échecs doivent être connus

Dans la prochaine leçon on discutera de la gestion des erreurs dans des tâches parallèles qui retournent plusieurs résultats. En attendant, souvenez-vous : si vous avez "tiré quelque chose", vérifiez si ça a bien atteint la cible !

7. Erreurs typiques en travaillant avec des tâches fire-and-forget

Erreur n°1 : Ignorer les exceptions dans le fire-and-forget.
Les débutants espèrent que les exceptions "remonteront quelque part". Sans try-catch et sans logging, elles se perdent et mènent à des bugs non détectés.

Erreur n°2 : Utiliser async void en dehors des handlers d'événements.
De telles méthodes envoient les exceptions au gestionnaire global (par ex. AppDomain.UnhandledException), ce qui peut provoquer l'arrêt brutal de l'application.

Erreur n°3 : Trop attraper les exceptions.
Attraper toutes les exceptions dans la tâche peut masquer des problèmes qui devraient être traités dans le code appelant, compliquant le debug.

Erreur n°4 : Négliger le logging.
Sans logs des erreurs dans les tâches fire-and-forget, il est impossible de savoir s'il y a des échecs, surtout en production.

2
Mission
C# SELF, niveau 61, leçon 0
Bloqué
Gestionnaire global `UnobservedTaskException`
Gestionnaire global `UnobservedTaskException`
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION