Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 1024 minIntermédiaire

Exceptions : try, catch et exceptions personnalisées

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • attraper une exception avec try, catch et finally ;
  • lever l'exception adaptée à chaque situation ;
  • créer des exceptions personnalisées pour les règles métier ;
  • libérer une ressource avec using ;
  • repérer une mauvaise gestion des erreurs.

4 exercices corrigés1 schéma

Une exception est un objet qui signale une erreur survenue pendant l'exécution : un fichier introuvable, une saisie qui n'est pas un nombre, une règle de gestion violée. En C#, on la lève avec throw, et on l'attrape avec un bloc try … catch ; si personne ne l'attrape, le programme s'arrête. Ce chapitre explique le mécanisme, la hiérarchie des exceptions de .NET, la création d'exceptions personnalisées pour ton domaine métier, et les bonnes pratiques qui distinguent un code robuste d'un code qui cache ses erreurs.

Pourquoi des exceptions

Sans exception, une méthode qui échoue doit le dire par sa valeur de retour : -1, null, false. Chaque appelant doit tester ce code, et l'oubli est silencieux : le programme continue avec une valeur fausse. Une exception, elle, interrompt le déroulement normal et remonte jusqu'au code qui sait la traiter. Impossible de l'ignorer par mégarde.

Tu en as déjà levé : dans les propriétés du chapitre 3 et les constructeurs du chapitre 4, throw new ArgumentException(…) refuse une valeur invalide. Ce chapitre regarde maintenant l'autre côté : que fait le code appelant ?

try, catch et finally

csharp
Logiciel acrobat = new Logiciel("Adobe Acrobat Pro", 2);
string[] postes = ["PC-0001", "PC-0002", "PC-0003"];
foreach (string p in postes)
{
    try
    {
        acrobat.Installer(p);
        Console.WriteLine($"{p} : installé");
    }
    catch (LicencesEpuiseesException e)
    {
        Console.WriteLine($"{p} : refusé. {e.Message}");
        Console.WriteLine($"   logiciel = {e.NomLogiciel}, licences = {e.NbLicences}");
    }
    finally
    {
        Console.WriteLine($"   (tentative journalisée pour {p})");
    }
}
text
PC-0001 : installé
   (tentative journalisée pour PC-0001)
PC-0002 : installé
   (tentative journalisée pour PC-0002)
PC-0003 : refusé. Les 2 licences du logiciel Adobe Acrobat Pro sont toutes utilisées.
   logiciel = Adobe Acrobat Pro, licences = 2
   (tentative journalisée pour PC-0003)
BlocRôle
tryle code qui peut échouer ; à la première exception, la suite du bloc est abandonnée
catch (TypeException e)traite les exceptions de ce type (et de ses sous-types) ; e donne accès au message et aux détails
finallys'exécute toujours, qu'il y ait eu une exception ou non, même après un return : pour libérer, fermer, journaliser

Un try peut avoir plusieurs catch : le premier dont le type correspond est exécuté, les autres sont ignorés. Range-les donc du plus précis au plus général : un catch (Exception) placé avant un catch (FormatException) rendrait le second inaccessible, et le compilateur le refuse (erreur CS0160).

La hiérarchie des exceptions

Toutes les exceptions héritent de la classe System.Exception. Attraper un type attrape aussi toutes ses classes filles : c'est le polymorphisme appliqué aux erreurs.

ExceptionSignificationExemple
ArgumentExceptionun argument est invalidenom vide passé à un constructeur
↳ ArgumentNullExceptionun argument vaut null alors qu'il est obligatoireArgumentNullException.ThrowIfNull(demandeur)
↳ ArgumentOutOfRangeExceptionun argument sort de l'intervalle permisdurée négative
InvalidOperationExceptionl'opération est impossible dans l'état actuelrésoudre un ticket sans intervention
FormatExceptionun texte n'a pas le format attenduint.Parse("abc")
NullReferenceExceptionappel sur une référence nullpresque toujours un bug, à corriger plutôt qu'à attraper
KeyNotFoundExceptionclé absente d'un dictionnaired["PC-0009"]
ArithmeticException ↳ DivideByZeroExceptionerreur de calculdivision entière par zéro
IOException ↳ FileNotFoundExceptionerreur d'entrée-sortiefichier d'import absent
NotSupportedException, NotImplementedExceptionopération non prise en charge, pas encore écrite

Chaque exception porte des informations utiles : Message (le texte), StackTrace (la pile d'appels, les méthodes traversées), InnerException (l'exception d'origine, quand une erreur en a provoqué une autre) et GetType().Name (son type).

La propagation

Quand une méthode lève une exception sans l'attraper, l'exception remonte à la méthode qui l'a appelée, puis à celle qui a appelé celle-ci, et ainsi de suite, jusqu'au premier catch compatible. Chaque méthode traversée est interrompue (ses blocs finally s'exécutent au passage). Si aucune ne l'attrape, le programme s'arrête et affiche Unhandled exception avec la pile d'appels.

Program.cs : Maintry { … } catch (LicencesEpuiseesException e)InstallationService.Installer()pas de catch : la méthode est interrompuePoste.Installer()pas de catch : la méthode est interrompueLogiciel.Installer()throw new LicencesEpuiseesException(…);appelleappelleappelleremonte
L'exception remonte la pile d'appels jusqu'au premier catch compatible

C'est ce qui permet de séparer la détection de l'erreur (au plus près des données, dans la classe métier) de son traitement (au plus près de l'utilisateur, dans l'écran ou le programme principal). Logiciel.Installer sait que les licences manquent, mais ne sait pas s'il faut afficher une boîte de dialogue, écrire dans un journal ou renvoyer un code HTTP 409 : ce n'est pas son rôle.

Lever une exception : throw

csharp
public void Installer(string numPoste)
{
    if (!PeutInstaller)
    {
        throw new LicencesEpuiseesException(Nom, NbLicences);
    }
    postes.Add(numPoste);
}

throw interrompt immédiatement la méthode : la ligne postes.Add n'est pas exécutée, l'objet reste dans un état cohérent. Pour les vérifications d'arguments, .NET fournit des méthodes de garde qui lèvent la bonne exception avec un message standard :

csharp
public Logiciel(string nom, int nbLicences)
{
    ArgumentException.ThrowIfNullOrWhiteSpace(nom);
    ArgumentOutOfRangeException.ThrowIfNegative(nbLicences);
    Nom = nom;
    NbLicences = nbLicences;
}

throw peut aussi s'écrire dans une expression, après ?? ou dans un opérateur conditionnel : Demandeur = demandeur ?? throw new ArgumentNullException(nameof(demandeur));.

Créer ses propres exceptions

Les exceptions de .NET décrivent des erreurs techniques. Pour une règle de ton domaine, une exception personnalisée est plus parlante, et le code appelant peut l'attraper spécifiquement sans confondre avec une autre erreur. On crée une classe qui hérite d'Exception (ou d'une de ses filles), dont le nom se termine par Exception.

csharp
class GestParcException : Exception
{
    public GestParcException(string message) : base(message) { }
    public GestParcException(string message, Exception inner) : base(message, inner) { }
}

class LicencesEpuiseesException : GestParcException
{
    public LicencesEpuiseesException(string nomLogiciel, int nbLicences)
        : base($"Les {nbLicences} licences du logiciel {nomLogiciel} sont toutes utilisées.")
    {
        NomLogiciel = nomLogiciel;
        NbLicences = nbLicences;
    }

    public string NomLogiciel { get; }
    public int NbLicences { get; }
}

class TransitionInterditeException : GestParcException
{
    public TransitionInterditeException(string de, string vers)
        : base($"Passage de « {de} » à « {vers} » interdit.") { }
}

Trois bonnes habitudes :

  • une classe mère commune (GestParcException) permet d'attraper d'un coup toutes les erreurs métier de l'application, et seulement elles ;
  • le message est construit par l'exception elle-même, à partir de ses données : il sera identique partout ;
  • des propriétés (NomLogiciel, NbLicences) donnent au code appelant les détails dont il a besoin, sans analyser le texte du message.
csharp
Ticket t = new Ticket(12);
try
{
    t.Resoudre();
}
catch (GestParcException e)
{
    Console.WriteLine($"{e.GetType().Name} : {e.Message}");
}
// TransitionInterditeException : Passage de « ouvert » à « resolu » interdit.

C'est la situation de l'épreuve blanche n° 2 de l'E6 SLAM : quand le nombre d'installations atteint le nombre de licences, l'installation est refusée, et l'écran affiche un message clair.

Relancer une exception et l'envelopper

Parfois, un catch doit agir (journaliser, annuler une transaction) puis laisser l'exception continuer sa remontée. On écrit alors throw;, sans rien derrière :

csharp
static void Traiter()
{
    try
    {
        ChargerFichier();
    }
    catch (InvalidOperationException)
    {
        Console.WriteLine("journalisé, puis relancé");
        throw;              // relance la MÊME exception, avec sa pile d'appels d'origine
    }
}

throw e; relancerait aussi l'exception, mais en effaçant la pile d'appels d'origine : on ne saurait plus où l'erreur est née. Utilise toujours throw;.

Pour traduire une erreur technique en erreur métier, enveloppe-la : crée la nouvelle exception en lui passant l'ancienne comme InnerException.

csharp
try
{
    int.Parse("douze");
}
catch (FormatException e)
{
    throw new ImportException("Ligne 3 du fichier d'import invalide.", e);
}

Le code appelant reçoit une ImportException compréhensible, et le journal garde la cause exacte (e.InnerException est la FormatException).

Filtrer avec when

Une clause when ajoute une condition à un catch : il n'attrape l'exception que si la condition est vraie.

csharp
foreach (string saisie in new[] { "45", "-5", "abc" })
{
    try
    {
        int duree = int.Parse(saisie);
        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(duree);
        Console.WriteLine($"{saisie} : durée acceptée");
    }
    catch (FormatException)
    {
        Console.WriteLine($"{saisie} : ce n'est pas un nombre");
    }
    catch (ArgumentOutOfRangeException e) when (e.ActualValue is int v && v < 0)
    {
        Console.WriteLine($"{saisie} : durée négative");
    }
}
text
45 : durée acceptée
-5 : durée négative
abc : ce n'est pas un nombre

Avec la saisie "0", aucun catch ne correspondrait (la valeur n'est pas négative) : l'exception remonterait.

Exception ou valeur de retour ? Le modèle TryXxx

Une exception coûte cher (la pile d'appels est capturée) et interrompt le flux. Réserve-la aux situations exceptionnelles, que le code appelant ne pouvait pas raisonnablement prévoir ou empêcher. Pour un échec attendu et fréquent, propose une méthode TryXxx qui renvoie un bool, sur le modèle de int.TryParse :

csharp
public bool TryInstaller(string numPoste)
{
    if (!PeutInstaller)
    {
        return false;
    }
    postes.Add(numPoste);
    return true;
}
csharp
if (!acrobat.TryInstaller("PC-0004"))
{
    Console.WriteLine("PC-0004 : pas de licence, demande d'achat à créer");
}
SituationChoix
Saisie de l'utilisateur à convertirTryParse : une saisie invalide est normale
Argument invalide passé par un autre développeurexception : c'est un bug à corriger
Règle métier violée par une action de l'utilisateurexception métier, attrapée par l'écran ; ou méthode PeutXxx() à tester avant d'agir
Élément cherché qui peut ne pas existerTryGetValue, FirstOrDefault : renvoyer null ou false
Ressource indispensable indisponible (base, fichier de configuration)exception

Libérer les ressources : using et IDisposable

Un objet qui tient une ressource extérieure (fichier ouvert, connexion à la base, connexion réseau) doit la libérer, même en cas d'exception. Ces classes implémentent l'interface IDisposable et sa méthode Dispose(). L'instruction using appelle Dispose() automatiquement à la sortie du bloc, quoi qu'il arrive : c'est un try … finally écrit pour toi.

csharp
class Journal : IDisposable
{
    public Journal() => Console.WriteLine("journal ouvert");
    public void Ecrire(string message) => Console.WriteLine($"écrit : {message}");
    public void Dispose() => Console.WriteLine("journal fermé");
}
csharp
using (Journal j = new Journal())
{
    j.Ecrire("ouverture du ticket 12");
}
Console.WriteLine("après le using");
text
journal ouvert
écrit : ouverture du ticket 12
journal fermé
après le using

La forme courte using Journal j = new Journal(); (sans accolades) libère l'objet à la fin du bloc qui contient la déclaration. Tu l'utiliseras pour chaque connexion à MySQL : le chapitre SQL dans une application en montre des exemples.

Les bonnes pratiques

  • Attrape le type le plus précis possible. catch (Exception) n'a sa place qu'au sommet du programme, pour journaliser et afficher un message générique avant de s'arrêter proprement.
  • Ne corrige pas un bug par un catch. Une NullReferenceException ou une IndexOutOfRangeException signalent une erreur de programmation : corrige le code.
  • Sépare le message technique du message utilisateur. L'utilisateur voit « Impossible d'installer : plus de licence disponible » ; le journal garde le type, le message et la pile d'appels.
  • N'affiche jamais une pile d'appels à l'utilisateur d'une application en production. Elle révèle la structure interne du programme, des chemins de fichiers, parfois des requêtes : c'est une fuite d'information exploitable. Le cours Sécurité applicative traite la journalisation et la gestion des erreurs du point de vue de la sécurité.
  • N'utilise pas les exceptions pour le déroulement normal (sortir d'une boucle, tester une existence) : c'est lent et illisible.

Exercices

Exercice 1 sur 4 Suivre le flux d'exécution Réussi À revoir

Donne la chaîne renvoyée par Tester("12"), Tester("0") et Tester("abc").

csharp
static string Tester(string saisie)
{
    string trace = "";
    try
    {
        trace += "A";
        int n = int.Parse(saisie);
        trace += "B";
        int r = 100 / n;
        trace += "C";
    }
    catch (FormatException)
    {
        trace += "F";
    }
    catch (ArithmeticException)
    {
        trace += "D";
    }
    finally
    {
        trace += "Z";
    }
    return trace;
}
Voir la réponseMasquer la réponse de l'exercice 1
  • Tester("12") renvoie ABCZ : aucune exception, puis le finally.
  • Tester("0") renvoie ABDZ : la division entière par zéro lève une DivideByZeroException, fille d'ArithmeticException, donc attrapée par le second catch ; C n'est jamais ajouté.
  • Tester("abc") renvoie AFZ : int.Parse lève une FormatException avant B.

Dans les trois cas, le finally s'exécute.

Tu avais trouvé ?

Exercice 2 sur 4 Une exception métier Réussi À revoir

Écris l'exception TicketIntrouvableException, levée quand on cherche un ticket par un numéro qui n'existe pas. Elle porte le numéro cherché dans une propriété et construit elle-même le message « Aucun ticket ne porte le numéro 42. ». Puis écris la méthode string Trouver(int numero) d'une classe Depot qui range les titres dans un Dictionary<int, string> et lève cette exception si le numéro est absent.

Voir la réponseMasquer la réponse de l'exercice 2
csharp
class TicketIntrouvableException : Exception
{
    public TicketIntrouvableException(int numero)
        : base($"Aucun ticket ne porte le numéro {numero}.")
    {
        Numero = numero;
    }

    public int Numero { get; }
}

class Depot
{
    private readonly Dictionary<int, string> tickets = new() { [1] = "Écran noir" };

    public string Trouver(int numero) =>
        tickets.TryGetValue(numero, out string? t) ? t : throw new TicketIntrouvableException(numero);
}

d.Trouver(42) lève l'exception ; un catch (TicketIntrouvableException e) peut afficher e.Message et utiliser e.Numero. Dans GestParc, elle hériterait de GestParcException. Selon l'usage, une méthode Ticket? TrouverOuNull(int numero) serait aussi acceptable : si chercher un numéro inexistant est fréquent (une recherche saisie par l'utilisateur), l'absence n'est pas exceptionnelle.

Tu avais trouvé ?

Exercice 3 sur 4 Critiquer une gestion d'erreurs Réussi À revoir

Relève les quatre mauvaises pratiques de ce code et propose une version corrigée.

csharp
public decimal ValeurDuParc(List<Materiel> parc)
{
    decimal total = 0;
    try
    {
        foreach (Materiel m in parc)
        {
            total += m.ValeurNette(DateOnly.FromDateTime(DateTime.Today));
        }
    }
    catch (Exception e)
    {
        Console.WriteLine(e.StackTrace);
    }
    return total;
}
Voir la réponseMasquer la réponse de l'exercice 3
  1. catch (Exception) attrape tout, y compris les bugs (NullReferenceException si un élément de la liste est nul).
  2. L'erreur est avalée : la méthode renvoie un total partiel, faux, comme si tout allait bien.
  3. La pile d'appels est affichée à l'utilisateur : inutile pour lui, et une fuite d'information.
  4. Une classe métier écrit dans la console : elle décide de l'affichage à la place de la couche de présentation, qui ne sera peut-être pas une console (formulaire, API).

Ici, rien dans la boucle ne justifie un try : si une erreur survient, c'est un bug ou une donnée invalide, et l'appelant doit le savoir. Version corrigée :

csharp
public decimal ValeurDuParc(IEnumerable<Materiel> parc, DateOnly date)
{
    ArgumentNullException.ThrowIfNull(parc);
    return parc.Sum(m => m.ValeurNette(date));
}

La date passe en paramètre, ce qui rend aussi la méthode testable (chapitre 15).

Tu avais trouvé ?

Exercice 4 sur 4 Quelle exception lever ? Réussi À revoir

Pour chaque situation, choisis l'exception la plus adaptée.

  1. Le constructeur de Intervention reçoit une durée de −15 minutes.
  2. On appelle Resoudre() sur un ticket déjà résolu.
  3. Le constructeur de Ticket reçoit null comme demandeur.
  4. Le fichier postes.csv contient « douze » dans la colonne du prix.
  5. On tente d'installer un logiciel dont toutes les licences sont utilisées.
Voir la réponseMasquer la réponse de l'exercice 4
  1. ArgumentOutOfRangeException (ThrowIfNegativeOrZero).
  2. InvalidOperationException, ou mieux l'exception métier TransitionInterditeException.
  3. ArgumentNullException (ArgumentNullException.ThrowIfNull(demandeur)).
  4. FormatException levée par decimal.Parse, à envelopper dans une ImportException qui précise la ligne ; ou decimal.TryParse pour signaler la ligne sans exception.
  5. L'exception métier LicencesEpuiseesException, fille de GestParcException.

Tu avais trouvé ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Égalité, Equals, GetHashCode, ToString et recordsRevoir le sommaire du cours