Guide pour la gestion des exceptions


30 08 2006

Cet article marche pour tout langage objet, mes lecteurs ruby, .net, java et php peuvent donc lire 😉 . Je ne parlerais ici que des exceptions non checkés car ce sont aujourd’hui les exceptions les plus répandues (et les checkés n’existent pas en .net…)

La gestion des exceptions est problématique c’est un fait. Qu’on soit développeur débutant ou confirmé, au moment de traiter une exception on se pose toujours les mêmes questions :

  • Est-ce que je l’attrape ou je la laisse remonter ?
  • Qu’est ce que je met dans le finally ?
  • Est-ce que je l’encapsule ?

Évidemment comme tout problème complexe, il n’y a pas de réponse définitive et tout est question de contexte. Pour ma part, j’applique une méthode qui, à défaut d’être complète, me permet d’évaluer rapidement dans quel contexte je suis et de décider de la solution à apporter.

1. Algorithme de décision

La première question que je me pose lors de l’appel d’un code qui risque de lever une exception est : Qu’est-ce que mon programme doit faire si l’exception arrive ?

La réponse à cette question tombera toujours dans deux catégories :

* J’ai une réponse de substitution et le programme peut continuer.

Par exemple, si j’ai une exception lors de la génération d’une couleur, j’ai prévu dans les spécifications de prendre une couleur par défaut. Dans ce cas, on pourra écrire ceci :

try {  couleur = GenerationCouleur() } catch( GenException ) {   couleur = CouleurParDefaut }

Attention dans ce cas à n’attraper que l’exception dont j’avais prévu la substitution et non un vilain catch( Exception ).

* Je n’ai pas de reprise particulière au problème.

L’exemple le plus commun est un plantage de l’accès à la base de données. Si je suis en train d’enregistrer un nouvel utilisateur et que mon accès sql se met en vrac, il est évident que je n’aurais aucun moyen de contournement pour terminer correctement mon action.

L’idée est donc d’admettre qu’on ne peut rien faire et de laisser remonter l’exception. De cette façon, l’exception va remonter les couches d’appels jusqu’à un appelant qui lui aura une réponse à apporter. Dans le cas d’une application web, il est commun que le seul qui aura une réponse à apporter sera le conteneur web (la brique la plus haute) qui affichera une jolie page d’erreur et s’occupera de logger tout ça…

2. Rethrow

Un autre type de code que l’on voit souvent :

try {   ... } catch( Exception ex) }    throw new MonException(ex) }

Ma prise de position sur ce sujet est qu’il faut éviter de catcher pour rien. En effet, quelle est la valeur ajoutée par la ligne ci-dessus ?

Cela ne fait qu’ajouter une indirection dans la cause réelle de l’erreur et provoque donc plus de travail pour le développeur qui doit comprendre ce qui s’est passé. Des stacktraces de 3 kms de long ne sont jamais agréables à lire…

La seule exception (je ne sais pas si le mot est bien choisi) est le développement d’un librairie ou d’un composant tiers. Dans ce cas effectivement, il sera plus propre de cacher les exceptions internes avec des jolis exceptions car la séparation est claire.

3. Fail fast

L’argument en faveur d’un ciblage précis des exceptions à attraper est le fail fast. L’idée du Fail fast est de provoquer un plantage quand une exception incontrôlée survient dans le but d’assainir le programme. En effet, il arrive souvent qu’on soit tenté d’avaler les exceptions avec du joli code comme ceci :

try {   ... } catch( Exception ) { // Toutes les exceptions  // on ne fait rien. On AVALE }

Et bien c’est MAL ! En effet, sur le moment effectivement si une erreur survient le client est content car son application n’affiche pas d’erreur et il peut continuer en apparence. Mais en réalité, le programme vient de basculer dans un état incohérent où l’état des objets en mémoire n’est plus garantie.

Résultat, on se retrouve avec un client qui se plaint d’avoir un message d’erreur 3 pages plus loin et alors là vient le courageux développeur qui va mettre 2 jours à comprendre le pourquoi du comment de l’erreur !

Dans le paradis du Fail Fast, le client serait tombé directement sur une page d’erreur au premier coup, il aurait pris son téléphone pour gueuler (dans les 2 cas il gueule car ça ne marche pas de toute façon 🙂 ) et hop corrigé en 15 mins, car effectivement c’était une mauvaise idée de caster cet objet de classe Personne en classe Agenda… (On sent le vécu)

4. Et le legacy ?

Le fail fast est une bonne idée sur les nouveaux développements mais que peut-on faire sur sur un applicatif existant -legacy- miné d’avaleuses (ne cherchez pas à visualiser) ?

Une mauvaise idée serait d’enlever tous les catchs {} vides car le client se retrouverait avec des erreurs sur des parties qui marchaient auparavant et ne comprendrait pas en quoi c’est mieux qu’avant. Ma proposition est plutôt d’ajouter dans tous les catchs un simple log. Par exemple :

catch (Exception ex) {   log.error(ex) }

Ainsi on pourra surveiller le comportemant du legacy et detecter les points d’erreurs qui n’étaient pas visibles avant. L’objectif étant évidemment de corriger le source au fur et à mesure de la détection de ces erreurs. Concernant les évolutions du programme, aucune pitié, on fail fast !

5. Conclusion

Le tour des rapide mais j’espère qu’il servira à enlever quelques ambiguités sur la réflexion que l’on porte aux exceptions.

Comme le sujet porte à débat, j’espère avoir des jolis commentaires sur ce post 🙂


Billets similaires

Actions

Informations

Une réponse à “Guide pour la gestion des exceptions”

1 09 2006
Eric (07:42:34) :

C’est un post tout-à-fait pertinent tant on se rend compte que c’est géré "aléatoirement" dans les projets.
Je pense que tu te poses les bonnes questions et tu y réponds bien.
Les exceptions me servent effectivement à arrêter l’éxécution d’un programme lorque le problème rencontré est fatal pour l’algorithme en cours.
Tu cites le bloc ‘finally’ sans en parler, je ne sais pas s’il existe dans le monde .Net. En gros, il permet d’éviter les problèmes d’incohérence des objets en mémoire dont tu parles pour rétablir l’état de ces objets à un état stable. Ca peut être rendre un objet null, le reconstruire ou se deconnecter d’une ressource en fermant un flux par exemple (on peut se trouver face à un lock du sytème d’exploitation si le flux ouvert sur un fichier n’est pas fermé).
Je pense qu’il est aussi important de faire la distinction entre exception technique et exception métier. En effet, on pense souvent à la séparation des couches du cas nominal (couche technique séparé du code métier qui devrait être le plus isolé techniquement possible) alors que la gestion des exception peut mélanger ces traitements de cas d’erreur (du traitement d’exception technique dans du métier par exemple). Mais je pense que c’est un peu du chipotage…