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 |
|---|---|---|---|
|
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.
GO TO FULL VERSION