• 20 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

J'ai tout compris !

Mis à jour le 10/03/2017

Gérez les erreurs : les exceptions

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Nous avons parlé rapidement des erreurs dans nos applications C# en disant qu'il s'agissait d'exceptions. C'est le moment d'en savoir un peu plus et surtout d'apprendre à les gérer ! Dans ce chapitre, nous allons apprendre comment créer et intercepter une exception.

Intercepter une exception

Rappelez-vous de ce code :

string chaine = "dix";
int valeur = Convert.ToInt32(chaine);

Si nous l’exécutons, nous aurons l’erreur suivante :

Image utilisateur

L’application nous affiche un message d’erreur et l’application « plante » lamentablement produisant un rapport d’erreur.
Ce qu’il se passe en fait, c’est que lors de la conversion, si le framework .NET n’arrive pas à convertir correctement la chaine de caractères en entier, il lève une exception. Cela veut dire qu’il informe le programme qu’il rencontre un cas limite qui nécessite d’être géré.
Si ce cas limite n’est pas géré, alors l’application plante et c’est le CLR qui intercepte l’erreur et qui fait produire un rapport au système d’exploitation.

Pourquoi une exception et pas un message d’erreur ?

L’intérêt des exceptions est qu'elles sont typées. Finis les codes d’erreurs incompréhensibles. C’est le type de l’exception, c'est-à-dire sa classe, qui va nous permettre d’identifier le problème.

Pour éviter le plantage de l’application, nous devons gérer ces cas limites et intercepter les exceptions.
Pour ce faire, il faut encadrer les instructions pouvant atteindre des cas limites avec le bloc d’instruction try…catch, par exemple :

try
{
    string chaine = "dix";
    int valeur = Convert.ToInt32(chaine);
    Console.WriteLine("Ce code ne sera jamais affiché");
}
catch (Exception)
{
    Console.WriteLine("Une erreur s'est produite dans la tentative de conversion");
}

Si nous exécutons ce bout de code, l’application ne plantera plus et affichera qu’une erreur s’est produite…

Nous avons « attrapé » l’exception levée par la méthode de conversion grâce au mot-clé catch.
Cette construction nous permet de surveiller l’exécution d’un bout de code, situé dans le bloc try et s’il y a une erreur, alors nous interrompons son exécution pour traiter l’erreur en allant dans le bloc catch.
La suite du code dans le try, à savoir l’affichage de la ligne avec Console.WriteLine, ne sera jamais exécuté car lorsque la conversion échoue, il saute directement au bloc catch.
Inversement, il est possible de ne jamais passer dans le bloc catch si les instructions ne provoquent pas d’erreur :

try
{
    string chaine = "10";
    int valeur = Convert.ToInt32(chaine);
    Console.WriteLine("Conversion OK");
}
catch (Exception)
{
    Console.WriteLine("Nous ne passons jamais ici ...");
}

Ainsi le code ci-dessus affichera bien uniquement que la conversion est bonne. Et en toute logique, il ne passera pas dans le bloc de traitement d’erreur, car il n’y en a pas eu.
Il est possible d’obtenir des informations sur l’exception en utilisant un paramètre dans le bloc catch :

try
{
    string chaine = "dix";
    int valeur = Convert.ToInt32(chaine);
}
catch (Exception ex)
{
    Console.WriteLine("Il y a un eu une erreur, plus d'informations ci-dessous :");
    Console.WriteLine();
    Console.WriteLine("Message d'erreur : " + ex.Message);
    Console.WriteLine();
    Console.WriteLine("Pile d'appel : " + ex.StackTrace);
    Console.WriteLine();
    Console.WriteLine("Type de l'exception : " + ex.GetType());
}

Ici, nous affichons le message d’erreur, la pile d’appel et le type de l’exception, ce qui donne :

Image utilisateur

D’une manière générale, la méthode ToString() de l’exception fournit des informations suffisantes pour identifier l’erreur :

try
{
    string chaine = "dix";
    int valeur = Convert.ToInt32(chaine);
}
catch (Exception ex)
{
    Console.WriteLine("Il y a un eu une erreur : " + ex.ToString());
}

Ce qui donne :

Image utilisateur

Les exceptions peuvent être de beaucoup de formes. Ici nous remarquons que l’exception est de type System.FormatException. Cette exception est utilisée en général lorsque le format d’un paramètre ne correspond pas à ce qui est attendu. En l’occurrence ici nous attendons un paramètre de type chaine de caractères qui représente un entier.
C’est une exception spécifique qui est dédiée à un type d’erreur précis.

Il faut savoir que comme beaucoup d’autres objets du framework .NET, les exceptions spécifiques dérivent d’une classe de base, à savoir la classe Exception. Il existe une hiérarchie entre les exceptions. Globalement, deux grandes familles d’exceptions existent : ApplicationException et SystemException. Elles dérivent toutes les deux de la classe de base Exception. La première est utilisée lorsque des erreurs récupérables sur des applications apparaissent, la seconde est utilisée pour toutes les exceptions générées par le framework .NET.
Par exemple, l’exception que nous avons vue, FormatException dérive directement de SystemException qui dérive elle-même de la classe Exception.

Le framework .NET dispose de beaucoup d’exceptions correspondant à beaucoup de situations. Notons encore au passage une autre exception bien connue des développeurs qui est la NullReferenceException. Elle se produit lorsqu’on essaie d’accéder à un objet qui vaut null. Par exemple :

Voiture voiture = null;
voiture.Vitesse = 10;

Vous aurez remarqué que dans la construction suivante :

try
{
    string chaine = "dix";
    int valeur = Convert.ToInt32(chaine);
}
catch (Exception ex)
{
    Console.WriteLine("Il y a un eu une erreur : " + ex.ToString());
}

nous voyons que le bloc catch prend en paramètre la classe de base Exception.
Cela veut dire que nous souhaitons intercepter toutes les exceptions qui dérivent de Exception ; c’est-à-dire en fait toutes les exceptions, car pour avoir une exception, elle doit forcément dériver de la classe Exception.
C’est utile lorsque nous voulons attraper toutes les exceptions. Mais savons-nous forcément quoi faire dans le cas de toutes les erreurs ?
Il est possible d’être plus précis afin de n’attraper qu’un seul type d’exception. Il suffit de préciser le type de l’exception attendu comme paramètre du catch.
Par exemple le code suivant nous permet d’intercepter toutes les exceptions du type FormatException :

try
{
    string chaine = "dix";
    int valeur = Convert.ToInt32(chaine);
}
catch (FormatException ex)
{
    Console.WriteLine(ex);
}

Cela veut par contre dire que si nous avons une autre exception à ce moment-là, du style NullReferenceException, l’exception ne sera pas attrapée. Ce qui fait que le code suivant va planter :

try
{
    Voiture v = null;
    v.Vitesse = 10;
}
catch (FormatException ex)
{
    Console.WriteLine("Erreur de format : " + ex);
}

En effet, nous demandons la surveillance de l’exception FormatException uniquement. Ainsi, l’exception NullReferenceException ne sera pas attrapée.

Intercepter plusieurs exceptions

Pour attraper les deux exceptions, il est possible d’enchainer les blocs catch avec des paramètres différents :

try
{
    // code provoquant une exception
}
catch (FormatException ex)
{
    Console.WriteLine("Erreur de format : " + ex);
}
catch (NullReferenceException ex)
{
    Console.WriteLine("Erreur de référence nulle : " + ex);
}

Dans ce code, cela veut dire que si le code surveillé provoque une FormatException, alors nous afficherons le message « Erreur de format … ». S’il provoque une NullReferenceException, alors nous afficherons le message « Erreur de référence nulle … ».
Notez bien que nous ne pouvons rentrer à chaque fois que dans un seul bloc catch.
Une autre solution serait d’attraper une exception plus généraliste par exemple SystemException dont dérive FormatException et NullReferenceException :

try
{
    // code provoquant une exception
}
catch (SystemException ex)
{
    Console.WriteLine("Erreur système : " + ex);
}

Par contre, il faut savoir que le code précédent attrape toutes les exceptions qui dérivent de SystemException. C’est le cas de FormatException et NullReferenceException, mais c’est aussi le cas pour beaucoup d’autres exceptions.
Lorsqu’on surveille un bloc de code, on commence en général par surveiller toutes les exceptions les plus fines possibles qui nous intéressent et on remonte en considérant les exceptions plus générales, jusqu’à terminer par la classe Exception :

try
{
    // code provoquant une exception
}
catch (FormatException ex)
{
    Console.WriteLine("Erreur de format : " + ex);
}
catch (NullReferenceException ex)
{
    Console.WriteLine("Erreur de référence nulle : " + ex);
}
catch (SystemException ex)
{
    Console.WriteLine("Erreur système autres que FormatException et NullReferenceException : " + ex);
}
catch(Exception ex)
{
    Console.WriteLine("Toutes les autres exceptions : " + ex);
}

À chaque exécution, c’est le bloc catch qui se rapproche le plus de l’exception levée qui est utilisé. C’est un peu comme une opération conditionnelle. .NET vérifie dans un premier temps que l’exception n’est pas une FormatException. Si ce n’est pas le cas, il vérifiera qu’il n’a pas à faire à une NullReferenceException. Ensuite, il vérifiera qu’il ne s’agit pas d’une SystemException. Enfin, il interceptera toutes les exceptions dans le dernier bloc de code car « Exception » étant la classe mère, toutes les exceptions sont interceptées avec ce type.

A noter qu’il est possible d’imbriquer les try…catch si cela s’avère pertinent. Par exemple :

string saisie = Console.ReadLine();
try
{
    int entier = Convert.ToInt32(saisie);
    Console.WriteLine("La saisie est un entier");
}
catch (FormatException)
{
    try
    {
        double d = Convert.ToDouble(saisie);
        Console.WriteLine("La saisie est un double");
    }
    catch (FormatException)
    {
        Console.WriteLine("La saisie n'est ni un entier, ni un double");
    }
}

Ce code nous permet de tester si la saisie est un entier. Si une exception se produit, alors nous tentons de le convertir en double. S’il y a encore une exception, alors nous affichons un message indiquant que les deux conversions ont échoué.

Le mot-clé finally

Nous avons vu que lorsqu’un code était surveillé dans un bloc try…catch, on sortait du bloc try si jamais une exception était levée, pour atterrir dans le bloc catch.
Cela veut dire qu’il est impossible de garantir qu’une instruction sera exécutée dans notre code, si jamais une exception nous fait sortir du bloc.
C’est là qu’intervient le mot-clé finally. Il permet d’indiquer que dans tous les cas, un code doit être exécuté, qu’une exception intervienne ou pas.
Par exemple :

try
{
    string saisie = Console.ReadLine();
    int valeur = Convert.ToInt32(saisie);
    Console.WriteLine("Vous avez saisi un entier");
}
catch (FormatException)
{
    Console.WriteLine("Vous avez saisi autre chose qu'un entier");
}
finally
{
    Console.WriteLine("Merci d'avoir saisi quelque chose");
}

Nous demandons une saisie utilisateur. Nous tentons de convertir cette saisie en entier. Si la conversion fonctionne, nous restons dans le bloc try et nous affichons :

Image utilisateur

Si la conversion lève une FormatException, nous affichons :

Image utilisateur

Dans les deux cas, nous passons obligatoirement dans le bloc finally pour afficher le remerciement.

Le bloc finally est utile par exemple quand il s’agit de libérer la mémoire, d’enregistrer des données, d’écrire dans un fichier de log, etc.

Il est important de remarquer que peu importe la construction, le bloc finally est toujours exécuté. Ainsi, même si on relève une exception dans le bloc catch :

try
{
    Convert.ToInt32("ppp");
}
catch (FormatException)
{
    throw new NotImplementedException();
}
finally
{
    Console.WriteLine("Je suis quand même passé ici");
}

nous afficherons toujours notre message...

Ou même lorsque nous souhaitons sortir d'une méthode avec le mot-clé return :

static void Main(string[] args)
{
    MaMethode();
}

private static void MaMethode()
{
    try
    {
        Convert.ToInt32("ppp");
    }
    catch (FormatException)
    {
        return;
    }
    finally
    {
        Console.WriteLine("Je suis quand même passé ici");
    }
}

Ici, le bloc finally est quand même exécuté.

Lever une exception

Il est possible de déclencher soi-même la levée d’une exception. C’est utile si nous considérons que notre code a atteint un cas limite, qu’il soit fonctionnel ou technique.
Pour lever une exception, nous utilisons le mot-clé throw, suivi d’une instance d’une exception. Imaginons par exemple une méthode permettant de calculer la racine carrée d’un double. Nous pouvons obtenir un cas limite lorsque nous tentons de passer un double négatif :

public static double RacineCarree(double valeur)
{
    if (valeur <= 0)
        throw new ArgumentOutOfRangeException("valeur", "Le paramètre doit être positif");
    return Math.Sqrt(valeur);
}

Ici, nous instancions une ArgumentOutOfRangeException en utilisant un constructeur à deux paramètres. Ceux-ci permettent d’indiquer le nom du paramètre ainsi que le message d’erreur. Cette exception est une exception du framework .NET utilisée pour indiquer qu’un paramètre est en dehors des plages de valeurs autorisées. C’est exactement ce qu’il nous faut ici.
Puis nous levons l’exception avec le mot-clé throw.
Nous pouvons utiliser la méthode ainsi :

static void Main(string[] args)
{
    try
    {
        double racine = RacineCarree(-5);
    }
    catch (Exception ex)
    {
        Console.WriteLine("Impossible d'effectuer le calcul : " + ex.Message);
    }
}

Ce qui donnera :

Image utilisateur

Il aurait été possible de lever une exception plus générique avec la classe de base Exception :

throw new Exception("Le paramètre doit être positif");

Mais n’oubliez pas que plus l’exception est finement typée, plus elle sera facile à traiter précisément. Cela permet d’éviter d’attraper toutes les exceptions dans le même catch avec la classe de base Exception.
À noter que lorsque notre programme rencontre le mot-clé throw, il arrête l’exécution du programme pour partir dans le bloc try…catch correspond (s’il existe). Cela veut dire qu’une méthode qui doit renvoyer un paramètre pourra compiler si son chemin se termine par une levée d’exception, comme c’est le cas pour le calcul de la racine carrée.

Propagation de l’exception

Il est important de noter que lorsqu’un bout de code se situe dans un bloc try…catch, tout le code qui est dessous est surveillé, même s’il y a plusieurs méthodes qui s’appellent les unes les autres.
Par exemple, si nous appelons depuis la méthode Main() une Methode1() qui appelle une Methode2() qui appelle une Methode3() qui lève une exception, alors nous serons capable de l’intercepter depuis la méthode Main() avec un try…catch :

static void Main(string[] args)
{
    try
    {
        Methode1();
    }
    catch (NotImplementedException)
    {
        Console.WriteLine("On intercepte l'exception de la méthode 3");
    }
}

public static void Methode1()
{
    Methode2();
}

public static void Methode2()
{
    Methode3();
}

public static void Methode3()
{
    throw new NotImplementedException();
}

À noter qu’une NotImplementedException est une exception utilisée pour indiquer qu’un bout de code n’a pas encore été implémenté.
Il est également possible d’attraper une exception, de la traiter et de choisir qu’elle continue à se propager.
Par exemple, imaginons que nous avons un bloc try…catch qui nous permet de surveiller tout notre programme et que nous ayons à surveiller un bout de code ailleurs dans le programme qui peut produire une situation limite :

static void Main(string[] args)
{
    try
    {
        MaMethode();
    }
    catch (Exception ex)
    {
        // ici, on intercepte toutes les erreurs possibles en indiquant qu'un problème inattendu s'est produit
        Console.WriteLine("L'application a rencontré un problème, un mail a été envoyé à l'administrateur ...");
        EnvoyerExceptionAdministrateur(ex);
    }
}

public static void MaMethode()
{
    try
    {
        Console.WriteLine("Veuillez saisir un entier :");
        string chaine = Console.ReadLine();
        int entier = Convert.ToInt32(chaine);
    }
    catch (FormatException)
    {
        Console.WriteLine("La saisie n'est pas un entier");
    }
    catch (Exception ex)
    {
        EnregistrerErreurDansUnFichierDeLog(ex);
        throw;
    }
}

J’ai une saisie à faire et à convertir en entier. Si la conversion échoue, je suis capable de l’indiquer à l’utilisateur en surveillant la FormatException. Par contre, si une exception inattendue se produit, je souhaite pouvoir faire quelque chose, en l’occurrence enregistrer l’exception dans un fichier, mais comme c’est un cas limite non prévu je souhaite que l’exception continue à se propager afin qu’elle soit attrapée par le bloc try…catch qui permettra d’envoyer un mail à l’administrateur.
Dans ce cas, j’utilise un catch généraliste pour traiter les exceptions inattendues afin de loguer l’exception puis j’utilise directement le mot-clé throw afin de permettre de relever l’exception.

Créer une exception personnalisée

Grâce au typage fort des exceptions, il est pratique d’utiliser un type d’exception pour reconnaitre un cas limite, comme une erreur de conversion ou une exception de référence nulle.
Nous allons pouvoir utiliser certaines de ces exceptions pour nos besoins, comme ce que nous avions fait avec l’exception ArgumentOutOfRangeException.
Bien sûr, il est possible de créer nous-mêmes nos exceptions afin de lever nos propres exceptions correspondant à des cas limites fonctionnels ou techniques.
Par exemple, imaginons un site d’e-commerce qui affiche une page correspondant au descriptif d’un produit afin de pouvoir le commander. Nous chargeons le produit. Si le produit n’est plus en stock alors il peut être judicieux de lever une exception afin que le site puisse gérer ce cas limite et afficher un message en conséquence.
Créons donc notre exception personnalisée : ProduitNonEnStockException

Pour ce faire, il suffit de créer une classe qui dérive de la classe de base Exception :

public class ProduitNonEnStockException : Exception
{
}

Qui pourra être utilisée ainsi :

static void Main(string[] args)
{
    try
    {
        ChargerProduit("TV HD");
    }
    catch (ProduitNonEnStockException ex)
    {
        Console.WriteLine("Erreur : " + ex.Message);
    }
}

public static Produit ChargerProduit(string nomProduit)
{
    Produit produit = new Produit(); // à remplacer par le chargement du produit
    if (produit.Stock <= 0)
        throw new ProduitNonEnStockException();
    return produit;
}

Il serait intéressant de pouvoir rendre l’exception plus explicite en modifiant par exemple la propriété message de l’exception. Pour ce faire, il suffit d’utiliser la surcharge du constructeur prenant une chaine de caractères en paramètre afin de pouvoir mettre à jour la propriété Message (qui est en lecture seule) :

public class ProduitNonEnStockException : Exception
{
    public ProduitNonEnStockException() : base("Le produit n'est pas en stock")
    {
    }
}

Nous pouvons également créer un constructeur qui prend le nom du produit en paramètre afin de rendre le message encore plus précis :

public class ProduitNonEnStockException : Exception
{
    public ProduitNonEnStockException(string nomProduit) : base("Le produit " + nomProduit + " n'est pas en stock")
    {
    }
}

Que nous pourrons utiliser ainsi :

public static Produit ChargerProduit(string nomProduit)
{
    Produit produit = new Produit(); // à remplacer par le chargement du produit
    if (produit.Stock <= 0)
        throw new ProduitNonEnStockException(nomProduit);
    return produit;
}

Ce qui donne :

Image utilisateur

À noter que pour construire cette exception personnalisée, nous avons dérivé de la classe de base Exception. Il aurait pu également être possible de dériver de la classe ApplicationException pour conserver une hiérarchie cohérente d’exceptions.

En résumé
  • Les exceptions permettent de gérer les cas limites d'une méthode.

  • On utilise le bloc try…catch pour encapsuler un bout de code à surveiller.

  • Il est possible de créer des exceptions personnalisées en dérivant de la classe de base Exception.

  • On peut lever à tout moment une exception grâce au mot-clé throw.

  • Les exceptions ne doivent pas servir à masquer les bugs.

Exemple de certificat de réussite
Exemple de certificat de réussite