1. Introduction
Commençons par une question importante : pourquoi ne peut-on pas simplement faire confiance aux méthodes asynchrones et espérer que tout fonctionnera toujours parfaitement ? Les opérations sur fichiers lèvent souvent des exceptions — le fichier peut avoir été supprimé, la mémoire peut manquer, il peut manquer des permissions ou le fichier peut être verrouillé. Dans du code synchrone vous attraperiez les problèmes avec le traditionnel bloc try-catch. Dans le code asynchrone la philosophie est la même, mais il y a des nuances : l'erreur peut se produire pas tout de suite lors de l'appel de la méthode, mais plus tard, quand l'opération est réellement exécutée.
Moment d'apparition de l'erreur
Dans le code synchrone en lisant via StreamReader.Read() l'exception sautera directement à la ligne d'appel — vous la prenez dans le catch, et voilà.
Dans le code asynchrone (await stream.ReadAsync()) l'erreur tombera non pas au démarrage de l'opération, mais directement au moment du await — quand la Task se termine avec une erreur. Si vous oubliez de mettre await, l'erreur peut rester «invisible» jusqu'à un certain temps.
2. Comment attraper les exceptions dans les méthodes asynchrones
Regardons tout de suite le modèle typique :
try
{
using FileStream fs = new FileStream("myfile.txt", FileMode.Open);
byte[] buffer = new byte[1024];
int bytesRead = await fs.ReadAsync(buffer, 0, buffer.Length);
// Traitement ultérieur...
}
catch (IOException ex)
{
Console.WriteLine("Erreur d'entrée-sortie : " + ex.Message);
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("Accès au fichier refusé : " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("Erreur inconnue : " + ex);
}
Oui-oui, c'est simple — on utilise le familier try-catch, mais dans une méthode asynchrone. Il est important que la méthode elle-même soit marquée par le mot-clé async, sinon le compilateur râlera.
Nuance importante : où placer le await
Task<int> readTask = fs.ReadAsync(buffer, 0, buffer.Length);
// ... ici on a accidentellement oublié await ou le traitement
Dans ce cas l'erreur, si elle survient, ira dans la Task elle-même (Task), et vous ne la connaîtrez pas tant que vous n'essayerez pas d'obtenir le résultat — par exemple via await readTask ou via la propriété Task.Exception. Si vous avez complètement oublié le await — la tâche peut se terminer avec une erreur et personne ne vous le dira.
3. Pourquoi le code asynchrone est sournois sans gestion des erreurs
Scénario 1 : «Fire and forget» — le piège du débutant
FileStream fs = new FileStream("file.txt", FileMode.Open);
byte[] buffer = new byte[8000];
fs.ReadAsync(buffer, 0, buffer.Length);
// Et ensuite le programme vit sa vie
L'opération de lecture part en «voyage parallèle», et si elle se termine par une erreur, aucun catch ne l'attrapera. L'exception sera cachée à l'intérieur de la Task. Ce pattern s'appelle «fire and forget» et dans les applications réelles il peut causer la perte d'erreurs critiques et des fuites de ressources.
Scénario 2 : Méthodes asynchrones sans await
Task t = MyAsyncMethod();
// ... ici on fait quelque chose, puis on oublie t
Les erreurs qui surviennent à l'intérieur de MyAsyncMethod ne remonteront pas tant que vous n'attendrez pas explicitement la tâche (await t ou t.Wait()).
4. Comment correctement attraper les erreurs provenant des tasks asynchrones
Stratégie 1 : Utilisez toujours await
try
{
await SomeFileOperationAsync();
}
catch (Exception ex)
{
Console.WriteLine("Quelque chose s'est mal passé : " + ex.Message);
}
Ainsi l'exception sera lancée directement à l'endroit de l'attente et ne se perdra pas.
Stratégie 2 : Gestion via .ContinueWith
Si pour une raison quelconque vous n'utilisez pas await, vous pouvez ajouter un gestionnaire d'erreurs via ContinueWith :
var task = fs.ReadAsync(buffer, 0, buffer.Length);
task.ContinueWith(t =>
{
if (t.Exception != null)
Console.WriteLine("Erreur lors de la lecture asynchrone : " + t.Exception.InnerException);
}, TaskContinuationOptions.OnlyOnFaulted);
Honnêtement ? Dans les applications C# modernes on fait rarement comme ça — async/await rend le code plus simple et plus propre.
Exceptions possibles lors du travail asynchrone avec des fichiers
- IOException — défaillance du disque, fichier introuvable, chemin trop long, appareil non disponible.
- UnauthorizedAccessException — permissions insuffisantes.
- ObjectDisposedException — le stream a été fermé avant la fin de l'opération.
- OperationCanceledException — l'opération a été annulée via un token d'annulation (CancellationToken).
5. Exemple : Lecture asynchrone avec gestion des erreurs
Ajoutons cette logique dans notre application :
using System;
using System.IO;
using System.Threading.Tasks;
namespace FileAsyncDemo
{
class Program
{
static async Task Main()
{
string path = "bigfile.txt";
byte[] buffer = new byte[4096];
try
{
using FileStream fs = new FileStream(path, FileMode.Open);
int bytesRead = await fs.ReadAsync(buffer, 0, buffer.Length);
Console.WriteLine($"Lu {bytesRead} octets depuis {path}");
}
catch (FileNotFoundException ex)
{
Console.WriteLine("Fichier introuvable : " + ex.Message);
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("Accès au fichier refusé : " + ex.Message);
}
catch (IOException ex)
{
Console.WriteLine("Erreur de lecture/écriture : " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("Autre erreur : " + ex.Message);
}
}
}
}
Remarque importante
Si vous n'utilisez pas await dans Main, et que vous faites seulement Task result = SomeAsyncMethod(); — les erreurs resteront «silencieuses» et se manifesteront plus tard, quand vous essayerez finalement d'obtenir le résultat.
6. Erreurs les plus fréquentes et pièges lors de la gestion des erreurs
On oublie de mettre await sur une méthode asynchrone — les erreurs ne remontent pas à temps, le programme se comporte de manière imprévisible.
On n'entoure pas les appels asynchrones d'un try-catch — l'application plante au premier échec.
On ne traite que Exception, en ignorant des exceptions spécifiques comme UnauthorizedAccessException ou OperationCanceledException — au final le diagnostic est mauvais.
On utilise des tâches «fire and forget» sans journaliser explicitement les erreurs — les exceptions se perdent à l'intérieur de la Task.
On considère que si la tâche est terminée — tout s'est bien passé. Il faut explicitement attendre le résultat (await) et gérer les exceptions.
GO TO FULL VERSION