Initialisation des systèmes...

Baptiste.Dev
Retour aux notes
LangagesDébutantSérie : C# / .NET

C# : Les exceptions

try/catch/finally, exceptions custom, filtres when, bonnes pratiques - comment gérer les erreurs en C#.

6 min de lecture
Mis à jour il y a 4 mois
c#csharpexceptionstrycatchthrowerreurs

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 !

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é");
}

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 return est dans le try. 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}");
}

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;
}

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)
}

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}");
}

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;
    }
}

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");
}

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 using pour les ressources - c'est plus fiable qu'un try/finally manuel (voir le Garbage Collector)

Ce qu'il ne faut PAS faire

  • Ne rattrape pas Exception sans bonne raison - tu masques des bugs
  • N'utilise pas les exceptions pour le flow control - un if est 100x plus rapide qu'un try/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;
}

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 :

ExceptionQuand
NullReferenceExceptionAccès à un membre sur un objet null
ArgumentExceptionParamètre invalide
ArgumentNullExceptionParamètre null alors qu'il ne devrait pas
InvalidOperationExceptionOpération invalide dans l'état actuel de l'objet
IndexOutOfRangeExceptionIndex hors limites d'un tableau
FormatExceptionFormat de string invalide (Parse)
IOExceptionErreur d'entrée/sortie (fichier, réseau)
FileNotFoundExceptionFichier introuvable (hérite de IOException)
NotImplementedExceptionMéthode pas encore implémentée
StackOverflowExceptionRécursion infinie (non rattrapable)
OutOfMemoryExceptionPlus 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