C# : Les exceptions
try/catch/finally, exceptions custom, filtres when, bonnes pratiques - comment gérer les erreurs en C#.
C# : Les exceptions
Quand quelque chose se passe mal à l'exécution - un fichier qui n'existe pas, une division par zéro, un index hors limites - C# lève une exception. Si personne ne la rattrape, le programme plante.
Les exceptions sont le mécanisme standard pour signaler et gérer les erreurs en C#.
# Qu'est-ce qu'une exception ?
Une exception est un objet (une instance d'une classe qui hérite de System.Exception) qui contient des informations sur l'erreur :
- Message - description de l'erreur
- StackTrace - où dans le code l'erreur s'est produite
- InnerException - l'exception d'origine si elle a été wrappée
int[] nombres = { 1, 2, 3 };
Console.WriteLine(nombres[10]); // IndexOutOfRangeException !int[] nombres = { 1, 2, 3 };Console.WriteLine(nombres[10]); // IndexOutOfRangeException !Sans gestion, le programme s'arrête et affiche le message d'erreur + la stack trace.
# try / catch / finally
C'est la structure de base pour gérer les exceptions :
try
{
// Code qui peut lever une exception
int resultat = 10 / 0;
}
catch (DivideByZeroException ex)
{
// Code exécuté si cette exception spécifique est levée
Console.WriteLine($"Erreur : {ex.Message}");
}
finally
{
// Code exécuté dans TOUS les cas (erreur ou pas)
Console.WriteLine("Nettoyage terminé");
}try{ // Code qui peut lever une exception int resultat = 10 / 0;}catch (DivideByZeroException ex){ // Code exécuté si cette exception spécifique est levée Console.WriteLine($"Erreur : {ex.Message}");}finally{ // Code exécuté dans TOUS les cas (erreur ou pas) Console.WriteLine("Nettoyage terminé");}Les règles
- try - entoure le code risqué. Obligatoire.
- catch - rattrape l'exception. Peut être multiple (un par type d'exception).
- finally - s'exécute toujours, même si une exception est levée, même si un
returnest dans letry. Optionnel mais utile pour le nettoyage.
Catch multiple
On peut rattraper différents types d'exceptions avec des blocs catch séparés. L'ordre compte - du plus spécifique au plus général :
try
{
string contenu = File.ReadAllText("config.json");
int valeur = int.Parse(contenu);
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"Fichier introuvable : {ex.FileName}");
}
catch (FormatException ex)
{
Console.WriteLine("Le contenu n'est pas un nombre valide");
}
catch (Exception ex)
{
// Catch-all pour toute autre exception
Console.WriteLine($"Erreur inattendue : {ex.Message}");
}try{ string contenu = File.ReadAllText("config.json"); int valeur = int.Parse(contenu);}catch (FileNotFoundException ex){ Console.WriteLine($"Fichier introuvable : {ex.FileName}");}catch (FormatException ex){ Console.WriteLine("Le contenu n'est pas un nombre valide");}catch (Exception ex){ // Catch-all pour toute autre exception Console.WriteLine($"Erreur inattendue : {ex.Message}");} Ne mets jamais catch (Exception) en premier - il attraperait tout et les catch spécifiques en dessous ne seraient jamais atteints.
# Lever une exception (throw)
On ne fait pas que rattraper les exceptions - on peut aussi les lever quand une situation invalide est détectée :
void Retirer(decimal montant)
{
if (montant <= 0)
throw new ArgumentException("Le montant doit être positif");
if (montant > solde)
throw new InvalidOperationException("Solde insuffisant");
solde -= montant;
}void Retirer(decimal montant){ if (montant <= 0) throw new ArgumentException("Le montant doit être positif"); if (montant > solde) throw new InvalidOperationException("Solde insuffisant"); solde -= montant;}throw interrompt immédiatement l'exécution de la méthode. L'exception remonte la pile d'appels jusqu'à trouver un catch qui la gère.
Re-throw
Dans un catch, on peut relancer l'exception après l'avoir loguée :
catch (Exception ex)
{
Console.WriteLine($"Erreur loguée : {ex.Message}");
throw; // relance l'exception ORIGINALE (préserve la stack trace)
}catch (Exception ex){ Console.WriteLine($"Erreur loguée : {ex.Message}"); throw; // relance l'exception ORIGINALE (préserve la stack trace)} Utilise throw; (sans argument) et pas throw ex;. La deuxième forme écrase la stack trace originale, ce qui rend le debug beaucoup plus difficile.
# Filtres when (C# 6+)
Les filtres when permettent d'ajouter une condition au catch sans rattraper l'exception si la condition est fausse :
try
{
HttpResponseMessage response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
Console.WriteLine("Page introuvable (404)");
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
{
Console.WriteLine("Accès refusé (401)");
}
catch (HttpRequestException ex)
{
Console.WriteLine($"Erreur HTTP : {ex.StatusCode}");
}try{ HttpResponseMessage response = await client.GetAsync(url); response.EnsureSuccessStatusCode();}catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound){ Console.WriteLine("Page introuvable (404)");}catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized){ Console.WriteLine("Accès refusé (401)");}catch (HttpRequestException ex){ Console.WriteLine($"Erreur HTTP : {ex.StatusCode}");}L'avantage par rapport à un if dans le catch : si le when est faux, l'exception continue de remonter au lieu d'être avalée silencieusement.
# Exceptions custom
Pour les erreurs spécifiques à ton domaine, crée tes propres exceptions :
class SoldeInsuffisantException : Exception
{
public decimal SoldeDemandé { get; }
public decimal SoldeActuel { get; }
public SoldeInsuffisantException(decimal demande, decimal actuel)
: base($"Solde insuffisant : {demande} demandé, {actuel} disponible")
{
SoldeDemandé = demande;
SoldeActuel = actuel;
}
}class SoldeInsuffisantException : Exception{ public decimal SoldeDemandé { get; } public decimal SoldeActuel { get; } public SoldeInsuffisantException(decimal demande, decimal actuel) : base($"Solde insuffisant : {demande} demandé, {actuel} disponible") { SoldeDemandé = demande; SoldeActuel = actuel; }}Le base(...) appelle le constructeur de Exception(string message). C'est cette string qu'on retrouve ensuite dans ex.Message quand on rattrape l'exception.
void Retirer(decimal montant)
{
if (montant > solde)
throw new SoldeInsuffisantException(montant, solde);
solde -= montant;
}
try
{
compte.Retirer(1000);
}
catch (SoldeInsuffisantException ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine($"Il manque {ex.SoldeDemandé - ex.SoldeActuel} euros");
}void Retirer(decimal montant){ if (montant > solde) throw new SoldeInsuffisantException(montant, solde); solde -= montant;} try{ compte.Retirer(1000);}catch (SoldeInsuffisantException ex){ Console.WriteLine(ex.Message); Console.WriteLine($"Il manque {ex.SoldeDemandé - ex.SoldeActuel} euros");}Une exception custom permet de transporter des données contextuelles (ici le solde demandé et le solde actuel) que le code appelant peut utiliser.
# Bonnes pratiques
Ce qu'il faut faire
- Catch spécifique - rattrape le type d'exception le plus précis possible, pas
Exception - Throw tôt - détecte les erreurs le plus tôt possible (validation des paramètres en début de méthode)
- Log avant de relancer - si tu ne peux pas gérer l'erreur, logge-la et relance avec
throw; - Utilise
usingpour les ressources - c'est plus fiable qu'untry/finallymanuel (voir le Garbage Collector)
Ce qu'il ne faut PAS faire
- Ne rattrape pas
Exceptionsans bonne raison - tu masques des bugs - N'utilise pas les exceptions pour le flow control - un
ifest 100x plus rapide qu'untry/catch - Ne fais pas de catch vide -
catch { }avale l'erreur silencieusement, c'est le pire anti-pattern - N'utilise pas
throw ex;- ça écrase la stack trace
// MAL - flow control par exception
try
{
int valeur = int.Parse(input);
}
catch (FormatException)
{
valeur = 0; // utilise les exceptions comme un if
}
// BIEN - TryParse pour le flow control
if (!int.TryParse(input, out int valeur))
{
valeur = 0;
}// MAL - flow control par exceptiontry{ int valeur = int.Parse(input);}catch (FormatException){ valeur = 0; // utilise les exceptions comme un if} // BIEN - TryParse pour le flow controlif (!int.TryParse(input, out int valeur)){ valeur = 0;}Quand lever une exception ?
- Situation exceptionnelle et inattendue (fichier corrompu, connexion perdue, argument invalide)
- Le code appelant ne peut pas vérifier la condition avant l'appel
- L'erreur empêche la méthode de remplir son contrat
Ne lève PAS d'exception pour des cas normaux (utilisateur qui entre un mauvais mot de passe, recherche sans résultat). Utilise un bool, un Result<T>, ou un code d'erreur.
# La hiérarchie des exceptions
Les exceptions en C# forment un arbre. Les plus courantes :
| Exception | Quand |
|---|---|
NullReferenceException | Accès à un membre sur un objet null |
ArgumentException | Paramètre invalide |
ArgumentNullException | Paramètre null alors qu'il ne devrait pas |
InvalidOperationException | Opération invalide dans l'état actuel de l'objet |
IndexOutOfRangeException | Index hors limites d'un tableau |
FormatException | Format de string invalide (Parse) |
IOException | Erreur d'entrée/sortie (fichier, réseau) |
FileNotFoundException | Fichier introuvable (hérite de IOException) |
NotImplementedException | Méthode pas encore implémentée |
StackOverflowException | Récursion infinie (non rattrapable) |
OutOfMemoryException | Plus de mémoire (non rattrapable) |
StackOverflowException et OutOfMemoryException ne peuvent pas être rattrapées avec un catch. Le CLR arrête le programme directement.
# La suite
Découvre aussi les autres piliers de la série :
- LINQ - requêtes intégrées au langage
- La mémoire - stack vs heap, value types vs reference types
- Les objets - classes, héritage, polymorphisme