[.NET] Comprendre la philosophie des exceptions sous .NET

Depuis la création du .NET Framework, j’ai vu beaucoup d’articles sur Internet concernant la gestion des exceptions d’un point de vue technique, mais très peu sur le concept et la philosophie de ce mécanisme.

Les exceptions offre un très grand confort au niveau de la gestion des erreurs, permettant aux développeurs de se concentrer beaucoup plus sur leur code fonctionnel. Malheureusement, la gestion des exceptions est souvent mal utilisée par les débutants…

J’ai donc écrit cet article, destiné aux débutants sous .NET, afin qu’ils puissent mieux comprendre la philosophie de ce mécanisme et mieux la maitriser.

J’ai donc divisé cet article en 4 parties :

La naissance des exceptions

Avant de commencer à expliquer en détail les exceptions, nous allons dans un premier temps essayer de comprendre qu’elle était l’origine des exceptions.

Les erreurs d’exécution.

Une application se déroule en exécutant une série d’instructions, mais l’une d’elle ne fonctionne pas comme prévue. Prenons par exemple une instruction qui consiste à écrire des données sur un périphérique réseau. Si durant l’exécution de cette instruction, la connexion avec le périphérique réseau est interrompue, celle-ci provoquera une erreur d’exécution.

Le plus souvent les erreurs d’exécution sont traitées en appelant des fonctions qui renvoient une valeur (code erreur) indiquant qu’une erreur s’est produite. On retourne 0 si tout s’est bien passé, une autre valeur dans le cas contraire, représentant le code d’erreur. Ainsi lorsque l’on fait appel à une fonction, il suffit de tester son résultat pour savoir si une erreur d’exécution s’est produite.

Ce mécanisme a été généralisé par l’utilisation des bases de données relationnelles, après chaque requête on a pris l’habitude de tester le code erreur pour s’assurer qu’elle s’était bien déroulée.

D’autres erreurs d’exécution peuvent être produites par un « bogue », c’est-à-dire un cas particulier qui n’a pas été testé par le développeur. Un exemple simple est l’ouverture d’un fichier dont on n’a pas testé son existence…

Dans ces deux cas, les erreurs d’exécution sont des situations qui ne correspondent pas à ce que l’on cherche à réaliser fonctionnellement à notre application.

La stabilité d’une application

Une notion importante qu’il faut garder à l’esprit quand on parle des erreurs d’exécution, est la « stabilité d’une application ».

Toute application exécute une série d’instructions de façon séquentielle (on peut appeler cela un flot d’exécution) :


Instruction i ;
Instruction i + 1 ;
Instruction i + 2 ;

Fonctionnellement, on cherche le plus souvent à ce que ces instructions s’exécutent sans aucun problème.

Cependant, il arrive parfois qu’une instruction « i » provoque une erreur d’exécution, et ne fournie pas le résultat attendu. Dans ce cas, les instructions suivantes « i + 1 », « i + 2 »,…etc nécessitant ce résultat, ne pourront pas s’exécuter correctement, ou si ce n’est pas le cas provoquer un résultat faussé !

A ce moment là, on dit que l’application devient « instable ». Pourquoi ? Tout simplement parce qu’une erreur « s’est glissée » dans le flot d’exécution et que si l’on continue à exécuter cette application on risque de travailler sur des données erronées et bien évidemment produire par la suite des résultats qui en sera de même…

Prenons par exemple une centrale nucléaire et plus particulièrement l’application qui se charge de contrôler la température du réacteur. Si une erreur d’exécution se produit durant l’exécution de cette application (le capteur de température ne répond pas), le fait de continuer à exécuter l’application peut produire des résultats incorrects et dans notre cas un mauvais contrôle de la température du réacteur, l’application est donc devenue « instable ». Dans ce cas que doit-on faire selon vous ? Laissez le réacteur continuer à fonctionner ou l’arrêter immédiatement…

Bien évidemment, on prendra soin d’arrêter le réacteur. Cependant il ne sera peut-être pas pratique d’arrêter le réacteur toutes les 10 minutes parce qu’une erreur d’exécution s’est produite durant l’exécution de l’application (En considérant que la remise en route de celui-ci est relativement longue). Il est tout à fait possible de traiter cette erreur et de continuer l’exécution normale de l’application…

Lorsqu’une erreur d’exécution se produit on doit donc :

  • Soit arrêter l’application
  • Soit traiter l’erreur et continuer l’exécution de l’application.

Le deuxième choix est bien évidemment celui que l’on cherchera toujours à réaliser… Car comme je l’ai illustré dans l’exemple précédent, il n’est pas pratique d’arrêter notre réacteur toutes les 10 minutes…

Cependant traiter une erreur d’exécution est plus difficile à réaliser. En effet, il faut s’assurer qu’après le traitement de celui-ci, l’application est de nouveau stable, c’est-à-dire qu’elle part sur des bonnes données, et que l’erreur n’affectera pas les instructions suivantes.

Voici un exemple en programmation pour illustrer la notion de stabilité d’application :

t = GetTemperature();
ControlerReacteur(t);

On considère que le méthode GetTemperature() retourne la température actuelle du réacteur. Et la méthode ControlerReacteur() se charge de contrôler la fusion du réacteur (plus le réacteur est chaud plus on ralenti la fusion et inversement).

On supposera que la méthode GetTemperature() retourne la valeur -1 (correspondant à un code d’erreur), s’il n’est pas possible d’obtenir la température du réacteur (si le capteur de température est endommagé par exemple).

Voici un exemple en programmation pour illustrer la méthode GetTemperature() dans ce cas :

public static int GetTemperature()
{
  if (capteur.EstEnPanne())
    return -1;

  return capteur.GetTemperature();
}
Public Shared Function GetTemperature() As Integer
  If capteur.EstEnPanne() Then
    Return -1
  End If

  Return capteur.GetTemperature()
End Function

En exécutant les 2 instructions précédentes, on peut se rendre compte que si la méthode GetTemperature() renvoie -1 et que l’on passe cette valeur à la méthode ControlerReacteur(), cette dernière risque de considérer que la température du réacteur est très basse et par conséquent augmenter la fusion du réacteur….
Ceci est un exemple typique d’une application devenue « instable » qui continue à fonctionner. C’est pour cela qu’il faut immédiatement après avoir obtenu une erreur d’exécution, traiter cette erreur, ou si ce n’est pas possible arrêter l’application.
Voici la solution à adopter si on souhaite arrêter l’application (ici nous considérons que si on arrête l’application, le réacteur s’arrêtera automatiquement) :

t = GetTemperature();
if (t == -1)
  ArreterApplication();
ControlerReacteur(t);
t = GetTemperature()
If t = -1 Then
  ArreterApplication()
End If

ControlerReacteur(t)

Dans le cas où on souhaite traiter l’erreur :

t = GetTemperature();
if (t == -1)
{
  t = GetTemperatureSurUnAutreCapteur();
  if (t == -1)
    return ArreterApplication();
}
ControlerReacteur(t);
t = GetTemperature()
If t = -1 Then
  t = GetTemperatureSurUnAutreCapteur()
  If t = -1 Then
    Return ArreterApplication()
  End If
End If
ControlerReacteur(t)

Dans le dernier exemple, on essaye en cas d’erreur, de demander la température à un autre capteur, et si on obtient toujours une erreur, on arrête l’application.

Comprenez que lorsqu’on essaye de traiter l’erreur, il faut s’assurer que l’application redevienne stable. Dans notre cas, ce n’est pas parce que l’on demande à un autre capteur la température du réacteur que forcement l’application redeviendra stable. C’est pour cela que l’on teste à nouveau le résultat de cette demande afin de s’assurer que le second capteur fonctionne correctement.

Le traitement des erreurs.

Si l’on revient aux anciens langages de programmation ne possédant pas de gestion des exceptions, on s’aperçoit que lors de l’appel d’une fonction (le plus souvent un appel d’une fonction Windows, dite « appel système »), le développeur doit systématiquement traiter les erreurs comme ceci :

int fd = open("fichier.doc", O_WRITE);
if (fd == -1)
{
 perror("Impossible de créer le fichier");
 exit(0);
}

Juste pour information, l’exemple précédent est un grand classique que l’on enseigne aux étudiants qui apprennent le langage C avec des appels systèmes.

Avec ce genre de traitement, on s’est très vite rendu compte des problèmes suivants :

  • Par manque de temps, les développeurs ne se préoccupaient plus de traiter les erreurs lors d’un appel à une fonction. Dans le cas où une erreur d’exécution se déclenchait, on continuait l’exécution d’une application devenue instable !
  • Traiter les erreurs consistait à fermer immédiatement l’application.
  • Traiter les erreurs, nécessite d’ajouter du code et donc des tests supplémentaires.
  • Les tests des traitements des erreurs sont le plus souvent très difficiles à mettre en œuvre.
  • Certains appels peuvent déclencher une centaine d’erreurs plus ou moins facilement testable (ou reproductible) ! Aussi par manque de temps, il est difficile voir très long de tester (et de prévoir) tous les cas possible d’erreur.

La naissance des exceptions.

Les exceptions sont nées pour palier à ces divers problèmes.

La nouvelle philosophie de ce concept est le suivant :

  • On code uniquement le code fonctionnel, si on s’aperçoit d’une erreur dans notre code on signale la présence de celle-ci. C’est ce que l’on appelle la levée d’une exception.
    Par exemple : Indiquer que le capteur de température de notre réacteur ne répond pas.
  • Si l’on souhaite traiter une erreur, on englobe une portion du code qui est susceptible de la déclencher et l’on traite. Sinon on ne fait rien !

Le premier point existait bien avant et consistait à signaler une erreur en renvoyant une valeur particulière (le plus souvent un code d’erreur).

Le deuxième point – qui aussi bizarre que cela puisse paraître – est facultatif. En effet, grâce au mécanisme des exceptions, on peut traiter une erreur ou laisser un code beaucoup « plus haut » (au niveau de la pile des appels des méthodes) s’en charger. Dans notre exemple, si l’on suppose que l’on dispose de la méthode suivante :

public static void FaireTournerReacteur()
{
  t = GetTemperature();
  ControlerReacteur(t);
}
Public Shared Sub FaireTournerReacteur()
  t = GetTemperature()
  ControlerReacteur(t)
End Sub

La méthode FaireTournerReacteur() pourrait se charger de traiter l’erreur qui serait levée par la méthode GetTemperature() si le capteur ne répond pas. Mais on peut aussi laisser l’appelant de la méthode FaireTournerReacteur() se charger de traiter cette erreur, ou alors l’appelant de l’appelant,..etc.

Dans le pire des cas, si aucun code n’est volontaire pour traiter cette erreur, l’application se fermera automatiquement ! Un tel mécanisme permet aux développeurs de s’affranchir à traiter à chaque appels de méthode s’il y a eu une erreur ou non. Et de fermer automatiquement l’application si l’on ne souhaite pas traiter l’erreur. Cela offre aussi une meilleure sécurité dans le cas où le développeur oublie de traiter un cas particulier d’erreur.

Nous allons maintenant, la partie suivante, détailler le mécanisme des exceptions.

Le mécanisme des exceptions

La levée d’une exception

Lorsque l’on souhaite signaler une erreur, il faut jeter une exception. Pour cela il existe sous .NET le mot clé throw.

Au lieu d’associer un code d’erreur, comme ce fût dans les langages précédents, on passe une instance d’un objet, contenant des informations concernant l’exception (emplacement de l’erreur, message d’erreur,…etc). Nous verrons plus-tard comment récupérer ce type d’information.

if (erreurDetectée == true)
  throw new InvalidOperationException("Opération invalide");
If erreurDetecté = True Then
  Throw New InvalidOperationException("Opération invalide")
End If

Le traitement d’une exception

Pour traiter une exception on doit désigner le bout de code qui est susceptible de déclencher une exception.

Voici un exemple :

try
{
  //Code susceptible de déclencher une exception
}
catch(SqlException e)
{
  //Traiter l'erreur
}
Try
  'Code susceptible de déclencher une exception
Catch e As SqlException
  'Traiter l'erreur
End Try

Le bloc try correspond à du code qui est susceptible de déclencher une exception.
Le bloc catch est le code qui sera exécuté lors du déclenchement d’un certain type d’exception. Le type d’exception que l’on souhaite traiter correspond au type de la variable paramètre du bloc catch (dans l’exemple précédent c’est SqlException).

Le déroulement de l’exécution d’une exception.

Maintenant voyons de plus près comment se déroule le mécanisme d’une exception.

Tout d’abord une exception nait et se déclenche dès que l’on appelle l’instruction « throw ». A ce moment là, .NET exécute le bloc catch attaché au bloc try où s’est produite la levée d’exception.

try
{
  throw new FileNotFoundException("Fichier.doc");
  //L'exception throw déclenché passe immédiatement
  //au bloc catch...

  //...
  //... ici le code ne sera pas exécuté !!!
}
catch (FileNotFoundException e)
{
  //Après déclenchement de l'exception
}
Try
  Throw New FileNotFoundException("Fichier.doc")
  'L'exception throw déclenché passe immédiatement
  'au bloc catch...

  '...
  '... ici le code ne sera pas exécuté !!!
Catch e As FileNotFoundException
  'Après déclenchement de l'exception
End Try

Il faut noter plusieurs choses importantes lors de la levée d’une exception :

  • Le code qui suit la levée de l’exception (throw) n’est plus exécuté!
    En effet, .NET réalise un saut directement sur le bloc catch. Il n’est alors plus possible de revenir à l’emplacement où s’est déclenché l’exception!
  • Lorsque vous déclenchez une exception, vous passez une instance d’un objet qui indique le type de l’exception (dans l’exemple précédent le type était FileNotFoundException). Le bloc catch qui est déclenché est celui qui est capable de traiter ce type d’exception. Si on avait déclenché une exception d’un autre type (par exemple SqlException), le bloc catch qui traite des exceptions de type FileNotFoundException ne serait pas exécuté.

Si une exception d’un certain type est déclenchée dans un bloc try, et que le bloc catch attaché ne traite ce type d’exception, c’est un bloc try/catch situé plus haut dans la pile des appels de méthode qui se chargera de traiter l’exception, et ainsi de suite…

try
{
  try
  {
    throw new FileNotFoundException("Fichier.doc");
    //L'exception throw déclenché passe immédiatement
    //au catch(FileNotFoundException) et non au bloc catch(SqlException)...
  }
  catch (SqlException e) //catch "a"
  {
    //...
  }

  //...
}
catch (FileNotFoundException e) //catch "b"
{
  //Après déclenchement de l'exception FileNotFoundException
}
Try
  Try
    Throw New FileNotFoundException("Fichier.doc")
    'L'exception throw déclenché passe immédiatement
    'au catch(FileNotFoundException) et non au bloc catch(SqlException)...
  Catch e As SqlException
    '...
  End Try

  '...
Catch e As FileNotFoundException
  'Après déclenchement de l'exception FileNotFoundException
End Try

Dans l’exemple précédent, une exception de type FileNotFoundException est déclenchée. Le bloc catch « a » ne gère pas ce type d’exception, celui-ci ne sera donc pas déclenché. Mais le code qui déclenche la levée de l’exception se trouve aussi englobé dans un autre « try », qui contient un bloc catch traitant des exceptions de type FileNotFoundException. Si en remontant ainsi, il n’existe aucun bloc « try » susceptible de traiter une exception, cela signifie qu’il n’existe pas de code qui soit capable de traiter l’exception. A ce moment là votre application est automatiquement terminée. En débogage, Visual Studio pointe sur la ligne qui à déclenché l’exception et vous affiche le message associé à celle-ci.

L’analogie des exceptions que l’on peut faire avec les anciens langages de programmation est que les blocs try/catch, permettent de créer de manière dynamique et structuré des bouts de code goto/label.

Exécutez du code après levée ou non d’une exception.

Il existe un bloc de code supplémentaire facultatif signalé par le mot clé finally qui permet d’exécuter du code après l’exécution du code try ou catch. En d’autres termes ce code sera exécuté dès la sortie du bloc try ou catch.

Le plus souvent on utilise ce bloc afin d’exécuter un bout de code critique qui doit être exécuté dans tous les cas (une libération de ressource par exemple).

r = AllouerRessource();
try
{
  //Code susceptible de déclencher une exception
}
catch(SqlException e)
{
  //Traiter l'exception
}
finally
{
  LibérerRessource(r);
}
r = AllouerRessource()
Try
  'Code susceptible de déclencher une exception
Catch e As SqlException
  'Traiter l'exception
Finally
  LibérerRessource(r)
End Try

Dans cet exemple, si le code du bloc try ou catch s’exécute, le code du bloc finally sera exécuté afin de libérer la ressource.

Il est très important de comprendre que le code présent dans ce bloc est bien exécuté à la sortie du try ou catch et non « après » !

Voici un exemple :

public static int TestFinally()
{
  Console.WriteLine("avant try");
  try
  {
    Console.WriteLine("try");
    throw new FileNotFoundException("Fichier.doc");
  }
  catch (FileNotFoundException e)
  {
    Console.WriteLine("catch");
    return 0;
  }
  finally
  {
    Console.WriteLine("finally");
  }
}
Public Shared Function TestFinally() As Integer
  Console.WriteLine("avant try")
  Try
    Console.WriteLine("try")
    Throw New FileNotFoundException("Fichier.doc")
  Catch e As FileNotFoundException
    Console.WriteLine("catch")
    Return 0
  Finally
    Console.WriteLine("finally")
  End Try
End Function

Dans cet exemple, certains pourraient penser que le bloc « finally » ne sera jamais exécuté à cause du « return 0 », qui comme tout le monde le sait, effectue un retour sur l’appelant de la méthode. Et bien c’est complètement faux ! Le bloc finally est exécuté immédiatement dès que l’on sort du bloc try ou catch. Le résultat produit dans la console par ce bout de code est le suivant :

avant try
try
catch
finally

Paramètre d’une exception

Lorsque vous déclenchez une exception vous passez à l’instruction throw une instance d’une classe dérivée de la classe System.Exception.

Cette instance est récupérée par le bloc « catch » comme ceci :

try
{
  //Code susceptible de déclencher des exceptions de type MonTypeException
}
catch(MonTypeException e)
{
  //Traiter les exceptions de type MonTypeException.
}
Try
  'Code susceptible de déclencher des exceptions de type MonTypeException
Catch e As MonTypeException
  'Traiter les exceptions de type MonTypeException.
End Try

On peut donc lors de la levée d’une exception passer des paramètres au bloc catch afin de pouvoir traiter les erreurs d’exécution en conséquence :

catch(MonTypeException e)
{
  if (e.Pourquoi == "En panne ")
    AfficherMessage(" Le réacteur est en panne ! ");
}
Catch e As MonTypeException
  If e.Pourquoi = "En panne" Then
    AfficherMessage("Le réacteur est en panne !")
  End If
End Try

Le type d’objet que vous passez durant la levée d’une exception est important ! En effet, c’est le type d’objet qui représente le type d’exception que vous déclenchez et que pouvez choisir de traiter.

Il existe par exemple, dans le Framework .NET les classes suivantes :

  • ArgumentNullException : On a reçu un paramètre null dans une méthode.
  • SqlException : Erreur au niveau de l’accès aux données SQL Server

Toutes ces classes dérivent de la classe System.Exception et représente un type d’erreur spécifique. Cela permet de filtrer les exceptions que l’on souhaite traiter sans récupérer celles qui ne nous intéressent pas.

Traiter plusieurs types d’exceptions

Il est tout à fait possible de traiter plusieurs types d’exception, pour cela il faut spécifier autant de bloc catch que nécessaire.

try
{
  //Code susceptible de déclencher une exception de type SqlException ou FileNotFoundException
}
catch(SqlException e)
{
  //Traiter les exceptions de type SqlException
}
catch(FileNotFound e)
{
  //Traiter les exceptions de type FileNotFound
}
Try
  'Code susceptible de déclencher une exception de type SqlException ou FileNotFoundException
Catch e As SqlException
  'Traiter les exceptions de type SqlException
Catch e As FileNotFound
  'Traiter les exceptions de type FileNotFound
End Try

Héritage des classes et généralisation des erreurs.

Les classes d’exceptions peuvent bien évidement, hériter de plusieurs classes, il est ainsi possible de traiter une famille de classe sans avoir à spécifier toutes les classes explicitement.

Par exemple la classe IOException est la classe de base des classes suivantes :

  • DirectoryNotFoundException
  • EndOfStreamException
  • FileNotFoundException
  • FileLoadException
  • PathTooLongException

Ainsi si l’appel d’une méthode est susceptible de déclencher une de ces exceptions, on peut y procéder ainsi :

try
{
  //Code susceptible de déclencher des exceptions de type
  //IOException, DirectoryNotFoundException, FileNotFoundException...etc
}
catch(IOException e)
{
  //Traiter les exceptions de type IOException
  //(et donc de type DirectoryNotFoundException ou
  //EndOfStreamException, ...etc).
}
Try
  'Code susceptible de déclencher des exceptions de type
  'IOException, DirectoryNotFoundException, FileNotFoundException...etc
Catch e As IOException
  'Traiter les exceptions de type IOException (et donc de type
  'DirectoryNotFoundException ou EndOfStreamException, ...etc).
End Try

Il faut cependant noter une remarque très importante :

Lorsque vous avez plusieurs blocs « catch », .NET essaye d’exécuter en priorité le premier bloc « catch », qui contient comme type de paramètre, un type identique ou dérivée de celui qui est déclenché par la levée de l’exception. En d’autres termes, si vous avez 2 blocs catch, l’un qui doit traiter une exception spécifique (par exemple FileNotFoundException) et l’autre toutes les autres types d’exceptions concernant les erreurs entrées/sorties (System.IOException), il faut dans ce cas commencer à mettre dans le premier bloc catch, l’exception la plus spécifique (ici FileNotFoundException) et ensuite celle qui est plus générale.

Voici un exemple de ce qui faut faire :

try
{
  //Code susceptible de déclencher des exceptions de type
  //IOException, DirectoryNotFoundException, FileNotFoundException...etc
}
catch(FileNotFoundException e)
{
  //Traiter uniquement les exceptions de type FileNotFoundException.
}
catch(IOException e)
{
  //Traiter toutes les exceptions de type IOException sauf celles de type
  //FileNotFoundException qui seront traités par
  //le bloc catch précédent.
}
Try
  'Code susceptible de déclencher des exceptions de type
  'IOException, DirectoryNotFoundException, FileNotFoundException...etc
Catch e As FileNotFoundException
  'Traiter uniquement les exceptions de type FileNotFoundException.
Catch e As IOException
  'Traiter toutes les exceptions de type IOException sauf celles de type
  'FileNotFoundException qui seront traités par
  'le bloc catch précédent.
End Try

Si maintenant on inverse l’ordre, le deuxième bloc catch ne sera jamais appelé car le premier est prioritaire et peut traiter les exceptions de type FileNotFoundException :

try
{
  //Code susceptible de déclencher des exceptions de type
  //IOException, DirectoryNotFoundException, FileNotFoundException...etc
}
catch(IOException e)
{
  //Traiter toutes les exceptions de type IOException.
}
catch(FileNotFoundException e)
{
  //Ne sera jamais appelé car le bloc précédent sera
  //exécuté lors de la levée d'une exception
  //de type FileNotFoundException.
}
Try
  'Code susceptible de déclencher des exceptions de type
  'IOException, DirectoryNotFoundException, FileNotFoundException...etc
Catch e As IOException
  'Traiter toutes les exceptions de type IOException.
Catch e As FileNotFoundException
  'Ne sera jamais appelé car le bloc précédent sera
  'exécuté lors de la levée d'une exception
  'de type FileNotFoundException.
End Try

Que trouve-t-on dans une exception ?

Avant de conclure cette partie, j’aimerai détailler les informations que l’on dispose dans la classe de base Exception. Bien évidemment, en héritant de cette classe pour vos propres exceptions, vous pouvez y ajouter d’autres propriétés.

La propriété Message

C’est bien évidemment la propriété la plus importante de la classe Exception. Elle représente un message destiné aux développeurs pour leur expliquer d’où vient exactement le problème.

La propriété StackTrace

C’est la deuxième propriété la plus importante de la classe Exception. Elle est automatiquement remplie lorsque l’on déclenche une exception. Elle contient l’état de la pile des appels de méthode au moment de la levée des exceptions. Attention, cette propriété ne contient pas le chemin du « petit Poucet » de tout le déroulement de l’application jusqu’à l’exception, mais bien l’état de la pile courante des appels de méthode.

Imaginons que nous avons la méthode suivante :

public static void UneMethode()
{
  for(int i=0 ; i<=5 ; i++)
    UneAutreMethode(i);
}

public static void UneAutreMethode(int i)
{
  if (i == 5)
    throw new UneException();
}
Public Shared Sub UneMethode()
    Dim i As Integer
    For i = 0 To 5
      UneAutreMethode(i)
    Next
End Sub

Public Shared Sub UneAutreMethode(ByVal i As Integer)
  If i = 5 Then
    Throw New UneException()
  End If
End Sub

Vous n’aurez pas le résultat suivant dans la propriété StackTrace (la dernière méthode appelée étant en haut de la pile) : UneAutreMethode(); UneAutreMethode(); UneAutreMethode(); UneAutreMethode(); UneAutreMethode(); UneMethode(); Mais bien l’état de la pile courante qui indique que l’on se trouve dans la méthode UneAutreMethode() qui a été appelé par la méthode UneMethode() : UneAutreMethode(); UneMethode(); La propriété StackTrace renvoie une chaine de caractères, elle est directement affichable et elle ne peut donc être exploitée correctement, si l’on veut rechercher une méthode précise dans la pile. Il existe pour cela la classe StackTrace, qui contient un constructeur prenant en paramètre une instance de la classe Exception. Une fois instanciée il est possible de récupérer élément de la pile (appelé StackFrame) via la propriété GetFrame() ou GetFrames().

La propriété TargetSite

Cette propriété contient la méthode qui a déclenché l’exception (et donc la méthode la plus haute dans la pile StackTrace).

La propriété InnerException

Lorsque plusieurs levées d’exception s’enchainent (c’est-à-dire que dans un bloc « catch » on re-déclenche une nouvelle exception), cette propriété contient l’exception qui a déclenché l’exception courante. Il existe une méthode Exception.GetBaseException() permettant de parcourir récursivement les propriétés InnerException, afin de trouvée la première exception qui est la source de cet enchainement.

La propriété Data

Cette propriété contient un dictionnaire contenant des informations complémentaires à une exception que vous souhaitez ajouter. Cela permettra aux développeurs de se servir de ces informations dans le but de mieux traiter les exceptions. Il faut absolument utiliser cette propriété pour stocker vos propres informations complémentaires, car cela permettra aux développeurs de récupérer « génériquement », des informations complémentaires sur tout type d’exception, sans connaitre à l’avance le type d’exception. Evitez de stocker des informations complémentaires dans des variables membres de votre classe Exception. Voici un exemple qui montre comment stocker une valeur supplémentaire dans une classe exception dérivée.

public class MonException : Exception
{
  public MonException(int uneInformation)
  {
    this.Data["CleUneInformation"] = uneValeur;
  }

  public int UneValeur
  {
    get { return (int)this.Data["CleUneInformation"] ; }
  }
}
Public Class MonException
   Inherits Exception
  Public Sub New(ByVal uneInformation As Integer)
    Me.Data("CleUneInformation") = uneValeur
  End Sub

  Public ReadOnly Property UneValeur() As Integer
    Get
       Return CType(Me.Data("CleUneInformation"), Integer)
    End Get
  End Property
End Class

Méthodologie pour une bonne gestion des exceptions

Bien définir les exceptions

Les exceptions (anciennement les erreurs d’exécution) doivent être levées pour signaler qu’une erreur s’est produite durant l’exécution normale d’application. J’ai mis volontairement en gras le mot « normale », car il faut bien assimiler que les exceptions signale un état de l’application qui ne correspond pas ce que fonctionnellement on cherche à réaliser. Si l’on reprend l’exemple de la centrale nucléaire, le fonctionnel que l’on cherche à réaliser pour la méthode de GetTemperature(), est de retourner la température prise sur le capteur. Tous les problèmes qui se situent autour de cette action doivent être signalées par une exception.

public static int GetTemperature()
{
  if (capteur.EstEnPanne())
    throw new CapteurEnPanneException();

  return capteur.GetTemperature();
}
Public Shared Function GetTemperature() As Integer
  If capteur.EstEnPanne() Then
    Throw New CapteurEnPanneException()
  End If

  Return capteur.GetTemperature()
End Function

Observez qu’avec le mécanisme d’exception, on signal un problème en une ligne de code (la levée d’une exception). On ne se préoccupe plus de traiter le problème… On laissera quelqu’un d’autre à plus haut niveau s’en charger (ou alors dans le cas échéant l’application sera fermée).

Quand déclencher une exception ?

Quand j’étais petit, ma mère m’avais dit : « Si tu te blesses à la main, il faut surtout passer un désinfectant sur la plaie, car sinon ca risque de s’infecter, se surinfecter et dans le pire des cas on devra t’amputer de la main… » C’est exactement la même philosophie qu’il faut garder avec les exceptions ! Il faut déclencher les exceptions le plus-tôt possible dès que l’on trouve un défaut dans l’exécution du programme. En effet, si vous continuez à faire tourner votre programme en propageant le défaut, non seulement cela sera difficile à résoudre, mais en plus cela provoquera des effets secondaires très difficiles à corriger (Mauvaises valeurs présentes dans les tables du SGBD par exemple…).

Dans l’absolu aucune exception ne doit être traitée !

Gardez à l’esprit que dans le déroulement normale de l’application (c’est-à-dire que la connexion à la base de données fonctionne bien, pas de problème à l’ouverture des fichiers,…etc), il ne doit jamais y avoir d’exception qui soit déclenchée ! La levée d’une exception qu’elle provienne du .NET Framework ou de votre application signale un état anormale de votre application et le plus souvent un bogue dans votre programme. Dans l’absolu on ne doit donc jamais traiter des exceptions. Je dis bien dans l’absolu car en pratique, il y a des cas où il est intéressant de traiter les exceptions. Tous ces cas seront vus par la suite. Certains débutant me diront : « Mais dès fois on est obligé d’utiliser les exceptions pour vérifier que l’on peut bien déclencher une opération. Par exemple, l’ouverture d’un fichier ». Il est vrai que si l’on regarde la documentation du .NET Framework, une exception de type FileNotFoundException sera levée, si l’on essaye d’ouvrir un fichier qui n’existe pas. Or, je ferai une remarque à ces débutants en leur indiquant que, le fait qu’une exception soit déclenchée parce que le fichier n’existe pas, dénote un bogue dans l’application du fait que le développeur n’a pas testé l’existence du fichier avant l’appel de la méthode réalisant l’ouverture du fichier.

Tester les paramètres des méthodes publiques et protégées

Lorsque vous exposez des méthodes publiques qui seront visibles par des développeurs (et des utilisateurs mal intentionnés par la même occasion), il faut s’assurer que les paramètres que vous recevez sont correctes ! Pour cela on utilise les exceptions pour signaler qu’un paramètre est incorrect. Voici un exemple, avec une méthode qui récupère un client dans un tableau en fonction de son index donné.

public static void GetClient(Client[] clients, int index)
{
  if (clients == null)
    throw new ArgumentNullException("clients");

  if(index < 0 || index >= clients.Length)
    throw new ArgumentOutOfRangeException("index", "index doit être compris entre...");

  return clients[index];
}
Public Shared Sub GetClient(ByVal clients() As Client, ByVal index As Integer)
  If clients Is Nothing Then
    Throw New ArgumentNullException("clients")
  End If

  If index < 0 Or index >= clients.Length Then
    Throw New ArgumentOutOfRangeException("index", "index doit être compris entre...")
  End If

  Return clients(index)
End Sub

Remarquez que dans cet exemple, on teste la validité des deux paramètres. Contrairement à ce que l’on peut penser, tester la validité des paramètres n’est pas une perte de temps pour vous et votre équipe. En effet, si vous enlevez les tests de validité, vous vous retrouvez avec une exception de type NullReferenceException. Ce genre d’exception n’est pas du tout explicite, et fait comprendre aux développeurs qui utiliseront votre méthode que celle-ci est bogué ! Voici un exemple simple d’un appel à une méthode statique, dont l’un des paramètres à été volontairement mis à null. Dans ce cas, la méthode Connexion lève une exception de type NullReferenceException qui n’indique en aucun cas au développeur pourquoi une telle erreur est apparue… Est-ce un paramètre incorrect ? La méthode est-elle bogué ?…

Affichage d'une exception de type NullReferenceException dans Visual Studio

De plus, si le paramètre est propagé dans différents appel de méthodes, vous aurez énormément de mal à retrouver l’origine du problème. Cela rejoint ce que je disais dans la partie précédente, il faut déclencher les exceptions le plutôt possible, dès que l’on se rend compte que les variables actuelles du programme sont incorrectes. Pour les méthodes protégées la punition reste la même que pour les méthodes publiques, car les développeurs peuvent hériter de votre classe et les appelées directement.

Ne pas tester les paramètres des méthodes privées ou internes.

Tester les paramètres de toutes méthodes peut couter un certain temps d’exécution qui pourrait devenir non négligeable. Contrairement à ce que l’on a vu précédemment, il n’est pas nécessaire de tester la validité des méthodes privées et interne. En effet, elles ne seront jamais appelées par d’autres développeurs extérieurs à l’assembly que vous réalisez. Vous pouvez par conséquent vous faire confiance à vous-même (et à votre équipe) et donc ne pas tester la validité des paramètres de ces méthodes.

Lever une exception à l’emplacement du code qui pose problème

Après avoir lu les deux précédentes parties, vous allez très certainement vous dire, pourquoi ne pas faire une boite à outils qui s’occuperait de tester la validité des paramètres et de déclencher une exception si besoin.

public static void TestParametreNonNull(object parametre, string nomParametre)
{
  if (parametre == null)
    throw new ArgumentNullException(nomParametre) ;
}
Public Shared Sub TestParametreNonNull(ByVal parametre As Object, ByVal nomParametre As String)
  If parametre Is Nothing Then
    Throw New ArgumentNullException(nomParametre)
  End If
End Sub

Exemple de code utilisant notre méthode outil :

public void UneMethodePublic(UnObject neDoitPasEtreNull)
{
  TestParametreNonNull(neDoitPasEtreNull, "neDoitPasEtreNull") ;
}
Public Sub UneMethodePublic(ByVal neDoitPasEtreNull As UnObject)
TestParametreNonNull(neDoitPasEtreNull, "neDoitPasEtreNull")
End Sub

L’idée de factoriser le contrôle de validité des paramètres est fort intéressant, mais le problème réside sur l’emplacement où est déclenchée l’exception. En effet, l’exception sera toujours déclenchée dans la méthode outil qui réalise le test de validité. Or, la levée d’une exception génère une information importante qui est l’emplacement où s’est déclenchée cette dernière. Le fait de lever une exception toujours au même endroit, n’aidera pas le développeur sur l’origine de l’exception. Vous devez donc toujours déclencher une exception à l’emplacement du code qui pose problème. Rien ne vous empêche de réaliser une exception qui indique si un paramètre est incorrect :

public static bool TestParametreNonNull(object parametre)
{
  if (parametre == null)
    return false;

  return true;
}
Public Shared Function TestParametreNonNull(ByVal parametre As Object) As Boolean
If parametre Is Nothing Then
Return False
End If

Return True
End Function

On peut ainsi utiliser cette méthode comme ceci :

public void UneMethodePublic(UnObject neDoitPasEtreNull)
{
  if (TestParametreNonNull(neDoitPasEtreNull) == false)
    throw new ArgumentNullException("neDoitPasEtreNull") ;
}
Public Sub UneMethodePublic(ByVal neDoitPasEtreNull As UnObject)
If TestParametreNonNull(neDoitPasEtreNull) = False Then
Throw New ArgumentNullException("neDoitPasEtreNull")
End If
End Sub

Il est aussi tout à fait possible de créer une méthode qui se chargera de créer une exception, et de déclencher celle-ci au moment voulue :

public static ArgumentNullException CreerExceptionParametreNull(string nomParametre)
{
  return new CreerExceptionParametreNull(nomParametre);
}
Public Shared Function CreerExceptionParametreNull(ByVal nomParametre As String) As ArgumentNullException
Return New CreerExceptionParametreNull(nomParametre)
End Function

On peut ainsi utiliser cette nouvelle méthode comme ceci :

public void UneMethodePublic(UnObject neDoitPasEtreNull)
{
  if (TestParametreNonNull(neDoitPasEtreNull) == false)
    throw CreerExceptionParametreNull("neDoitPasEtreNull");
}
Public Sub UneMethodePublic(ByVal neDoitPasEtreNull As UnObject)
If TestParametreNonNull(neDoitPasEtreNull) = False Then
Throw CreerExceptionParametreNull("neDoitPasEtreNull")
End If
End Sub

En résumé, il faut absolument que le mot clé « throw » qui indique la levée d’une exception, se situe à l’endroit exacte du code qui pose problème.

S’assurer de la stabilité de l’application

Lorsqu’une exception est levée, si vous la traiter, et la laisser poursuivre, il faut absolument s’assurer que l’application n’est pas instable ! Traiter une exception, et laisser une application instable à continuer de fonctionner risque d’aggraver amplement la situation ! (Pensez à la centrale nucléaire avec le réacteur qui continue à fonctionner, alors que le capteur de température ne fonctionne plus !).

Traiter une exception, puis la « re-lever »…

Il arrive parfois que le traitement de l’exception consiste tout simplement à exécuter une action particulière, mais l’application est toujours instable alors on veut quand même que l’exception soit levée. Pour cela il suffit tout simplement de re-déclencher l’exception dans le bloc « catch » en utilisant le mot clé throw :

public static void UneMéthode()
{
  OuvrirConnexionSGBD();
  try
  {
    //Code susceptible de déclencher un SqlException :
    ExecuterRequeteSQL() ;
  }
  catch(SqlException e)
  {
    FermerConnexion();
    throw e; //Re-lever l'exception
  }
}
Public Shared Sub UneMethode()
OuvrirConnexionSGBD()
Try
'Code susceptible de déclencher un SqlException :
ExecuterRequeteSQL()
Catch e As SqlException
FermerConnexion()
Throw e 'Re-lever l'exception
End Try
End Sub

Une remarque importante à noter, est qu’en procédant ainsi, vous perdez une information importante qui est l’emplacement où s’est produite l’exception d’origine. En effet, en re-levant une exception, vous déclenchez une exception à un nouvel emplacement. Si dans la méthode ExecuterRequeteSQL() on avait une méthode DéclencherException1() qui appelle à son tour une méthode DéclencherException2() qui se charge de déclencher l’exception on aurait dans la pile des appels (On suppose que l’appel à la méthode ExecuterRequeteSQL() se trouve à la ligne 16, et re-levée de l’exception à la ligne 64) : UneMethode() à la ligne 64 Or on aurait aimé avoir plustôt : DéclencherException2() à la ligne … DéclencherException1() à la ligne … ExecuterRequeteSQL() à la ligne … UneMethode() à la ligne 16 Pour palier à ce problème, il suffit d’utiliser la syntaxe suivante :

catch(SqlException e)
{
  FermerConnexion();
  throw;
}
Catch e As SqlException
FermerConnexion()
Throw
End Try

En procédant ainsi, on se retrouverait avec la pile des appels suivantes : DéclencherException2() à la ligne … DéclencherException1() à la ligne … ExecuterRequeteSQL() à la ligne … UneMethode() à la ligne 64 Même si on a récupéré l’emplacement d’origine où s’est déclenché l’exception, on ne sera plus à quel endroit du bloc « try » de la méthode UneMéthode s’est déclenché l’exception. La partie suivante, va vous expliquer comment garder la trace complète d’une exception précédemment déclenchée, en l’encapsulent dans une nouvelle exception.

Changer le type d’une exception

Dès fois une exception d’un certain type est levée, mais on souhaite en re-lever une autre d’un autre type. Par exemple si l’on reprend l’exemple de notre réacteur, on traite une exception de type CapteurEndommagé, mais on souhaite re-déclencher une exception d’un autre type qui saurait : EtatReacteurInconnu

public static void FaireTournerReacteur()
{
  try
  {
    t = GetTemperature();
  }
  catch(CapteurEndommagé e)
  {
    throw new EtatReacteurInconnu() ;
  }
  ControlerReacteur(t);
}
Public Shared Sub FaireTournerReacteur()
Try
t = GetTemperature()
Catch e As CapteurEndommagé
Throw New EtatReacteurInconnu()
End Try
ControlerReacteur(t)
End Sub

La encore, comme je l’ai précisé dans la partie précédente, vous perdez l’information d’où s’est déclenché réellement l’exception. Pour remédier à ce problème il est possible d’encapsuler une instance d’une exception dans une autre :

catch(CapteurEndommagé e)
{
  throw new EtatReacteurInconnu(e) ;
}
Catch e As CapteurEndommagé
Throw New EtatReacteurInconnu(e)

La classe de base Exception, possède une propriété InnerException qui contient l’instance de l’exception encapsulée. Dans notre cas, c’est une instance de type CapteurEndommagé qui est encapsulé dans une instance de type EtatReacteurInconnu. L’encapsulation des exceptions permet de retrouver étape par étape les exceptions qui se sont successivement déclenchée lorsque l’on obtient une levée d’exception. Une chose importante à noter, est que l’encapsulation des exceptions dans vos propres classes d’exception, ne fonctionnera uniquement que si vous respectez le pattern spécifié dans la partie suivante.

Créer une classe spécifique pour les exceptions.

Beaucoup de type d’erreurs peuvent déclencher des instances d’exceptions de classe prédéfinie par le .NET Framework. L’exemple le plus rependu étant la classe ArgumentNullException, qui est utilisé si un paramètre null est passé à une méthode. Cependant, dans certains cas il est conseillé de créer ces propres types d’exception avec au minimum les constructeurs suivant (Il existe un code snippet en C# pour les gros fainéants) :

[Serializable]
public class MonException : Exception
{
  public MonException()
  {

  }
  public MonException(string message)
  : base(message)
  {

  }
  public MonException(string message, Exception inner)
  : base(message, inner)
  {

  }
  protected MonException(SerializationInfo info, StreamingContext context)
  : base(info, context)
  {

  }
}
<Serializable> _
Public Class MonException
Inherits Exception
Public Sub New()

End Sub
Public Sub New(ByVal message As String)

End Sub
Public Sub New(ByVal message As String, ByVal inner As Exception)

End Sub
Protected Sub New(ByVal info As SerializationInfo, ByVal context As StreamingContext)

End Sub
End Class

Ce pattern est absolument recommandé par Microsoft (Règle n°CA1032 de Microsoft.CSharp). Mais rien ne vous empêche d’ajouter vos propres constructeurs pour construire une exception avec beaucoup plus d’informations… Une chose importante à noter : Les classes exceptions doivent être sérializable ! Tout simplement parce qu’une exception peut être envoyé d’une application à une autre via un moyen de communication quelconque… (On retrouve cela dans Windows Communication Foundation par exemple). Il existe deux façons de nommer et d’utiliser une exception :

  • Soit vous nommer l’exception par un nom spécifique, représentant un type d’erreur bien spécifique qui est souvent utilisée et non présent dans le .NET Framework. Par exemple, une des classes indispensables à mettre dans sa bibliothèque d’outils : ArgumentStringEmptyException, qui représente un paramètre avec une chaîne vide.
  • Vous nommez l’exception avec un nom représentant globalement le fonctionnel que vous êtes en train de développer. Par exemple dans le .NET Framework, tout ce qui concerne les erreurs SQL Server dans ADO .NET sont des exceptions de type SQLException.

Les exceptions que vous créez ne doivent pas être forcement publique et peuvent donc être interne à un assembly. Une telle conception n’a pas grand intérêt et ne permettra au code extérieur de traiter l’exception.

La clause using

Lorsque vous utiliser des classes implémentant l’interface IDisposable, il faut penser à appeler la méthode Dispose(). Pour les langages C# et VB .NET, il existe une clause « using » permettant de créer un de ces objets et en « lui donnant une portée ». Dès que l’on sort de cette portée la méthode Dispose() est automatiquement appelée. Bien évidemment, comme on l’a vu dans la première partie, le déclenchement d’une exception, provoque un saut dans le code. Dans le cas de la clause using, si le déclenchement d’une exception provoque un saut en dehors de la portée de cette clause, la méthode Dispose() sera automatiquement appelé.

try
{
  using (StreamReader stream = new StreamReader(" MonFichier.txt "))
  {
    throw new UneException("Oups ! ") ;
  }

  ...
}
catch(UneException e)
{

}
Try
Using stream As StreamReader = New StreamReader(" MonFichier.txt "))
Throw New UneException("Oups !")
End Using

...
Catch e As UneException

End Try

Dans l’exemple précédent, au moment où l’exception est levée, le code passe immédiatement dans le bloc catch. Comme on réalise un saut vers du code se situant en dehors de la portée du bloc « using », la méthode Dispose() de l’objet StreamReader sera automatiquement appelé. Le fichier « MonFichier.txt » sera donc automatiquement fermé ! Il est fortement recommandé pour une programmation très sécurisée d’utiliser au maximum ce genre de clause.

Un duo gagnant : try/finally

Il arrive souvent que dans un bloc de code, vous allouez une ressource et que vous souhaitiez libérer celle-ci :

r = AllouerRessource();
...//Du code
LibérerRessource(r);
r = AllouerRessource()
...'Du code
LibérerRessource(r)

Le problème d’un tel code, c’est qu’en cas d’exception entre l’allocation et la libération, vous risquez de ne pas la libérer. On peut donc pour cela utiliser un bloc try et finally uniquement. Le bloc try, contient le bloc à protéger entre l’allocation et la libération de la ressource. Le bloc finally contient la libération de la ressource qui sera appelé en cas de levée ou non d’une exception !

r = AllouerRessource();
try
{
  ...//Code susceptible de déclencher une exception
}
finally
{
  LibérerRessource(r);
}
r = AllouerRessource()
Try
...'Code susceptible de déclencher une exception
Finally
LibérerRessource(r)
End Try

L’avantage de ce duo gagnant, c’est que l’on peut déclencher obligatoirement tout type de code après le bloc try, même si une exception est déclenchée ! A noter aussi que le bloc sera quand même appelé si aucun code n’est capable de traiter une exception qui serait levée dans le bloc « try ». La clause using est une alternative au duo try/finally pour libérer les ressources qui implémente l’interface IDisposable, préférez dans ce cas la clause using qui est plus simple à écrire.

Isoler les erreurs

Dans les applications critiques, il est possible d’utiliser le mécanisme des exceptions, afin d’isoler des parties d’une application devenue instable. Le cas le plus simple étant dans une application multithread. En cas d’une exception, on peut tuer un thread et laisser l’application continuer à s’exécuter. Il existe bien évidemment d’autres façons d’isoler différentes parties de son application, mais la philosophie reste toujours la même. Isoler une partie d’une application devenue instable, c’est la « surveiller » via le mécanisme d’exception. Pour cela on encapsule le bloc de code d’une partie de l’application à isoler dans un bloc « try », et on rajoute un bloc catch qui s’occupera de récolter tous les types d’exceptions. Dans ce bloc « catch », on réalise les traitements nécessaires afin de terminer, le plus proprement si possible, l’exécution de la partie de l’application qui pose problème. Un exemple : en imaginant que le code suivant s’exécute dans un thread :

public static void MethodeThread()
{
  try
  {
    ...//Execution du thread
    ...//Code susceptible de déclencher une exception
    ...//
  }
  catch(Exception e)
  {
    //Récolter tous les types d'exception (aussi bien gérable ou ingérable).

    Thread.CurrentThread.Abort();  //Tuer le thread courant
  }
}
Public Shared Sub MethodeThread()
Try
...'Execution du thread
...'Code susceptible de déclencher une exception
...'
Catch e As Exception
'Récolter tous les types d'exception (aussi bien gérable ou ingérable).

Thread.CurrentThread.Abort() 'Tuer le thread courant
End Try
End Sub

Comme vu précédemment, il faut s’assurer qu’après l’extinction brutale du thread, l’application soit toujours stable, c’est-à-dire que les variables partagées ne contiennent pas de mauvaises valeurs, d’autres threads ne sont pas dépendant du résultat du thread que l’on vient d’arrêter…etc. On retrouve ce genre d’isolation dans les applications hautement critique ou il n’est pas envisageable d’arrêter systématiquement l’application dès qu’une exception non gérée est levée. (SQL Server ou ASP .NET sont des exemples d’applications qui possèdent ce genre d’isolation). Un autre exemple d’isolation qui est facile à réaliser, ce sont les fenêtres Windows Forms. Lors d’une levée d’exception, il est tout à fait possible de fermer la fenêtre et de laisser l’application continuer à fonctionner. Mais comme je l’ai expliqué précédemment, il faut s’assurer qu’après la fermeture de la fenêtre, votre application redevienne dans un état stable.

Afficher un message d’erreur et l’envoyer au support technique

On peut traiter aussi les exceptions de manière générique afin de récupérer l’exception, afficher un message à l’écran et fermer l’application. C’est ainsi que l’on peut envoyer aussi un rapport d’erreur par mail à l’équipe recette de l’application afin de mieux comprendre où l’exception a été levée.

try
{
  ...//Code susceptible de déclencher une exception
}
catch(Exception e)
{
  MessageBox.Show("Oups... Désolé du dérangement !");
  EnvoyerRapportErreur(e, "support-technique[at]maboite.com");
  FermerApplication();
}
Try
...'Code susceptible de déclencher une exception
Catch e As Exception
MessageBox.Show("Oups... Désolé du dérangement !")
EnvoyerRapportErreur(e, "support-technique[at]maboite.com")
FermerApplication()
End Try

Un point à noter, le bloc « try » sera le plus souvent, le bloc le plus haut (dans la méthode main) afin de récolter toutes les exceptions non traitées de l’application.

Les dangers des exceptions

« La programmation par exception »

Le mécanisme des exceptions est coûteux en temps, il faut donc utiliser ce mécanisme que dans les cas exceptionnels et non pour obtenir le résultat d’une opération. Certains débutants utilisent par exemple ce mécanisme pour contrôler la saisie des utilisateurs, vérifier si un fichier existe, …etc Voici un exemple classique :

try
{
  File.Open("MonFichier.txt", FileMode.Open);
}
catch(FileNotFoundException e)
{
  MessageBox.Show("Le fichier ?MonFichier.txt' n'existe pas !");
}
Try
File.Open("MonFichier.txt", FileMode.Open)
Catch e As FileNotFoundException
MessageBox.Show("Le fichier 'MonFichier.txt' n'existe pas ! ")
End Try

Il faut bannir absolument ce code et préférer celui-ci qui sera beaucoup plus rapide :

if (File.Exists("MonFichier.txt"))
File.Open("MonFichier.txt", FileMode.Open);
If File.Exists("MonFichier.txt") Then
File.Open("MonFichier.txt", FileMode.Open)
End If

Traiter les exceptions de manière générique

Dans le .NET Framework tous les types d’exception héritent de la classe Exception. Certaines exceptions sont dites « intraitables » ou « ingérables », car une fois déclenchée, elles marquent le plus souvent une instabilité sévère du système ou une erreur de programmation qui est impossible à gérer. Par exemple :

  • NullReferenceException (Vous avez passez un paramètre null à une méthode).
  • NotEnoughMemoryException (Il n’y a plus assez de mémoire…).
  • …etc

Comme vous pouvez le constater ces exceptions une fois déclenchée, sont impossible à traiter (la première implique une correction du code, la deuxième implique que l’utilisateur libère de la mémoire). Il n’est donc pas recommandé, voir inutile de traiter ces exceptions. L’exception non gérable la plus connu, et la plus répandu dans les programmes en cours de développement étant l’exception NullReferenceException. Cette exception est déclenchée lorsque vous appelez une méthode membre d’une classe dont la référence est null. Ce genre d’exception est ingérable car elle dénote toujours une erreur de programmation et ne peux donc être traitée.

try
{
  String s = null;
  String s2 = s.Substring(4);
}
catch(NullReferenceException e)
{
  //Et alors on fait quoi ? On laisse un bogue qui est traité par une exception ? Ou on corrige le bogue ?
}
Try
Dim s As String = Nothing
Dim s2 As String = s.Substring(4)
Catch e As NullReferenceException
'Et alors on fait quoi ? On laisse un bogue qui est traité par une exception ? Ou on corrige le bogue ?
End Try

La version corrigée du précédent exemple serait tout simplement :

String s = "Les exceptions";
String s2 = s.Substring(4);  //s2 contient la chaîne "exceptions".
Dim s As String = "Les exceptions"
Dim s2 As String = s.Substring(4)  's2 contient la chaîne "exceptions".

Il faut cependant faire attention à ne pas traiter aussi les exceptions de manières générique comme ceci :

try
{
  ....//Du code susceptible de déclencher une exception.
}
catch(Exception e)
{
  .....//On récolte tous les types d'exceptions, traitable on non traitable.
}
Try
....'Du code susceptible de déclencher une exception.
Catch e As Exception
.....'On récolte tous les types d'exceptions, traitable on non traitable.
End Try

En procédant ainsi, vous risquerez de récupérer tous les types d’exceptions qui soient traitables ou non. Je vois énormément ce genre chez les débutants et surtout dans les développements en VB .NET. Ces débutants diront qu’ils utilisent ce bloc catch uniquement pour afficher des messages d’erreurs durant la mise au point de l’application.

try
{
  ....//Du code susceptible de déclencher une exception.
}
catch(Exception e)
{
  Debug.WriteLine(e.Message);
}
Try
....'Du code susceptible de déclencher une exception.
Catch e As Exception
Debug.WriteLine(e.Message)
End Try

Moi je leur répondrai deux choses (en leur tirant les oreilles) :

  • Soit vous supprimer le bloc catch, et laissez-le le merveilleux déboguer de Visual Studio faire le reste…
  • Soit – comme je l’ai dis durant tout cet article -, assurez qu’après l’exécution du catch l’application soient stable! Si ce n’est pas le cas, re-déclencher l’exception par mesure de sécurité.
catch(Exception e)
{
  Debug.WriteLine(e.Message);
  throw e;
}
Catch e As Exception
  Debug.WriteLine(e.Message)
Throw e
End Try

Ne jamais déclencher une exception générique.

Souvent les développeurs par manque de temps, déclenche une exception générique comme ceci :

if (reacteur.GetTemperature() == tropChaud)
 throw new Exception("Fait trop chaaaaaaaauuuuddd !");
If reacteur.GetTemperature() = tropChaud Then
Throw New Exception("Fait trop chaaaaaaaauuuuddd !")
End If

Ceci est fortement déconseillé, car cette exception oblige les programmeurs à attraper des exceptions génériques. Comme on l’a vu précédemment, il est aussi déconseillé d’attraper des exceptions de manière générique, de plus on n’informe pas le développeur sur le type exacte d’erreur. Tout est marqué dans un message d’erreur qui se trouve dans une chaîne de caractères…

N’affichez pas le détail d’une exception à l’écran !

Evitez absolument d’afficher un descriptif détaillé de l’exception (type, message, pile d’appels,…etc) à l’écran de l’utilisateur.

Des détails croustillant lors de la levée d'une exception...

Si cela peut paraitre du charabia pour les utilisateurs non informaticiens, cela représente des détails très intéressant pour les utilisateurs mal intentionnés. Il est donc fortement déconseillé, de fournir des informations importantes au niveau du message d’erreur des exceptions (par exemple la valeur d’un mot de passe, l’adresse IP du serveur sur lequel l’application souhaite s’y connecter…etc). J’ai rencontré beaucoup de sites en production réalisés par des débutants, qui laissaient afficher les messages d’erreurs d’ASP .NET lors d’une levée d’exception (Le plus souvent dans ce message d’erreur on y retrouve une requête SQL qui a échoué). Quelques jours plus tard, la base de données s’est retrouvée vide ! Cela implique d’un autre côté, d’éviter de fournir trop d’informations importante au niveau des exceptions. Ne fournissez en aucun cas des informations sensibles dans les messages des exceptions.

Conclusion

J’espère qu’à travers cet article vous comprennez mieux la philosophie des exceptions. Gardez bien à l’esprit qu’il faut :

  • Déclencher une exception dès que vous remarquez que votre application se trouve dans une situation anormale par rapport à son comportement fonctionnel souhaité.
  • Traiter une exception uniquement si vous êtes capable de remettre votre application dans un état stable.

Sachez aussi que dans une application bien structurée, si vous supprimez tous les blocs « catch » dans votre code source, il doit y rester uniquement le code fonctionnel de votre application.

Remerciements

Je tiens à remercier les personnes suivantes pour leurs remarques et la relecture de cet article :

  • AICHOUCH Mehdi
  • BERTHOUD Yannick
  • CARON Jean-Yves
  • CASABIANCA Florian
  • DUTHOO Laurent
Publié dans la catégorie .NET Framework.
Tags : , , , , , , , . TrackBack URL.

Pas de commentaire

1 Trackbacks