Journal : Qu'est-ce que bien gérer les erreurs dans ses programmes ?
Posté par Nicolas Boulay () le 14 décembre 2007
La question métaphysique du jour : comment bien gérer les erreurs ?
Il y a déjà plusieurs types d'erreurs: celles qui relèvent de la mauvaise utilisation de code, elle pourrait se traiter avec des assert(), il y a celle qui remonte un mauvais fonctionnement à l'étage du dessus, et celle entre les deux, qui peuvent servir parfois à effectuer des "scans" de fonctionnalités (genre on charge tous les noyau réseau un par un pour trouver le bon driver de sa carte).
Les problèmes surviennent lorsque du code qui devrait retourner un assert() et donc crasher en cas d'erreur font un simple "return error"; et qui a le malheur de faire une exécution partielle du code. En général, 2 ou 3 appels de fonctions différentes de la lib en question plus tard, tout va se viander aléatoirement.
Donc, il faut déjà prévoir quoi faire en cas d'erreurs au plus bas niveau et ne remonter qu'en cas d'impossibilité de gérer le problème à ce niveau et encore, en le faisant proprement (par exemple, un retour de malloc à 0 ?).
Proprement, cela veut dire peut-être de séparer fonction de test de validité des paramètres, et exécution proprement dite de la fonction, cela permet d'éviter les exécutions partielles (imaginez une fonction de lib qui retourne une erreur en s'étant exécuter à moitie dans un cas qui n'entraine pas une erreur fatal de l'ensemble).
Souvent dans les exemples d'utilisation, la gestion des erreurs est mise de coté pour éviter d'alourdir un exemple. Cela démontre déjà que la gestion d'erreur à tendance à brouiller l'algorithme de base. Je trouve que cela renforce le principe de base de bien séparer exécution et traitement d'erreur.
Il y a maintenant les gestions d'exception pour faire cela. Je n'ai jamais vraiment coder avec, mais je n'ai jamais non plus vu un véritable enthousiasme pour ce système.
J'aurais tendance à éviter toute gestion d'erreurs qui entraîne un crash. Aucun utilisateur n'aime voir un crash, surtout dans l'embarqué. Cela me rappelle une certaine central inertielle qui partait en autotest sur une exception Ada. J'aurais tendance à interdire formellement tout code qui interrompt la fonctionnalité.
Connaissez vous des règles génériques pour déterminer la conduite à tenir en cas de retour d'erreur ?
Qu'est-ce que vous conseillez donc pour faire propre ?
Il y a déjà plusieurs types d'erreurs: celles qui relèvent de la mauvaise utilisation de code, elle pourrait se traiter avec des assert(), il y a celle qui remonte un mauvais fonctionnement à l'étage du dessus, et celle entre les deux, qui peuvent servir parfois à effectuer des "scans" de fonctionnalités (genre on charge tous les noyau réseau un par un pour trouver le bon driver de sa carte).
Les problèmes surviennent lorsque du code qui devrait retourner un assert() et donc crasher en cas d'erreur font un simple "return error"; et qui a le malheur de faire une exécution partielle du code. En général, 2 ou 3 appels de fonctions différentes de la lib en question plus tard, tout va se viander aléatoirement.
Donc, il faut déjà prévoir quoi faire en cas d'erreurs au plus bas niveau et ne remonter qu'en cas d'impossibilité de gérer le problème à ce niveau et encore, en le faisant proprement (par exemple, un retour de malloc à 0 ?).
Proprement, cela veut dire peut-être de séparer fonction de test de validité des paramètres, et exécution proprement dite de la fonction, cela permet d'éviter les exécutions partielles (imaginez une fonction de lib qui retourne une erreur en s'étant exécuter à moitie dans un cas qui n'entraine pas une erreur fatal de l'ensemble).
Souvent dans les exemples d'utilisation, la gestion des erreurs est mise de coté pour éviter d'alourdir un exemple. Cela démontre déjà que la gestion d'erreur à tendance à brouiller l'algorithme de base. Je trouve que cela renforce le principe de base de bien séparer exécution et traitement d'erreur.
Il y a maintenant les gestions d'exception pour faire cela. Je n'ai jamais vraiment coder avec, mais je n'ai jamais non plus vu un véritable enthousiasme pour ce système.
J'aurais tendance à éviter toute gestion d'erreurs qui entraîne un crash. Aucun utilisateur n'aime voir un crash, surtout dans l'embarqué. Cela me rappelle une certaine central inertielle qui partait en autotest sur une exception Ada. J'aurais tendance à interdire formellement tout code qui interrompt la fonctionnalité.
Connaissez vous des règles génériques pour déterminer la conduite à tenir en cas de retour d'erreur ?
Qu'est-ce que vous conseillez donc pour faire propre ?
> Lire le journal (80 commentaires, moyenne: 2,4).
Vous avez demandé le commentaire #890593.



Ma réponse :
Question : Comment bien gérer ses erreurs ?
Réponse : ne pas en faire !
[^]Re: Ma réponse :
>J'aurais tendance à interdire formellement tout code qui interrompt la fonctionnalité.
Le principe de l'erreur (et encore plus en embarqué) c'est justement que ça interrompt en plein milieu de rien. (genre le bête plantage physique d'un équipement)
Pour moi, les exceptions c'est le top. En c#, des exceptions typées qui vont bien. Alors oui ça alourdit énormément. Ceci étant, c'est un problème récurrent : si tu veux gérer tous les cas d'erreurs possibles et imaginables, ton code sera hyper lourd. Après le tout est de trouver le juste milieu.
[^]Re: Ma réponse :
En C#, tu arrives à bien séparer la gestion d'erreur du reste ?
Parce que si ton code devient illisible, cela peut être pire.
Qu'est-ce que tu appelles des "exceptions typées" ?
[^]Re: Ma réponse :
En gros ton exception est une classe. Quand tu lances une exception, c'est l'instance d'une classe que tu envoies.
Ça va remonter tous les niveaux de la pile d'appel jusqu'à ce qu'on la capture, et le programme s'arrête si jamais aucune fonction ne la capture.
Donc ce qu'il devait vouloir dire, c'est que tu es libre de capturer les types d'exceptions que tu veux à un niveau donné du code, et à avoir un traitement différent à chaque fois.
Le principe des exceptions en soit n'est pas mauvais, c'est juste que ça reste très chiant à mettre en oeuvre (dans le sens où dans un projet relativement gros ça prends du temps, et qu'en général tu as autre chose en tête).
[^]Re: Ma réponse :
Il existe d'autre modèle que les exceptions ou le retour de message d'erreur ?
[^]Re: Ma réponse :
Oui, le pire de tous : une variable globale (au hasard, appelons-la errno)…
(Ouais, bon, j’exagère, en général, errno sert seulement à savoir quelle erreur s’est produite, la survenance de l’erreur elle-même est indiquée par un code retour spécial (souvent 0, en tout cas un résultat hors domaine), mais la technique de la variable globale à vérifier existe quand même…)
[^]Re: Ma réponse :
C'est le contraire: en général c'est -1 la valeur de retour pour une erreur, 0 signifiant que tout s'est bien passé. C'est d'ailleurs une hypothèse de très longue date des shells (genre `set -e' en sh: 'Exit immediately if a simple command [...] exits with a non-zero status.').
[^]Re: Ma réponse :
C'est vrai pour le shell mais pas en programmation C.
Il arrive de voir des fonctions de bibliothèques prendre comme convention 0=erreur, !=0=succes.
Ca permet des trucs du genre
if (fonction() ) {
}
[^]Re: Ma réponse :
En même temps, ça parait logique lorsqu'on retourne des pointeurs :)
[^]Re: Ma réponse :
Je parlais de petites lib pas très connues comme la libc: c'est généralement une valeur -1 qui est retournée en cas d'erreur. et 0 en cas de succès (quand ce la a du sens, pas quand il faut retourner un descripteur de fichier). Genre chown, ioctl (dans la majorité des cas), shutdown, sysctl, syscall, etc. Tout n'est question de convention et d'homogénéité, et on peut faire l'inverse si on veut.
[^]Re: Ma réponse :
Vi, vi, m’a gouru pour la libc (qui est le principal utilisateur de errno). Si je l’oublie si facilement, c’est parce que je fais comme tout le monde : je ne vérifie jamais la valeur de retour :oP
[^]Re: Ma réponse :
Tout n'est question de convention et d'homogénéité, et on peut faire l'inverse si on veut.
Oui, le problème c'est qu'il n'y a vraiment pas homogénéité dans les conventions d'une lib à l'autre.
D'où l'intérêt des exceptions qui permettent que tout soit clair sur la signification du retour.
[^]Re: Ma réponse :
Non, y a toujours moyen de faire pire :
http://quadaemon.free.fr/tribune.fortune
[^]Re: Ma réponse :
Il existe d'autre modèle que les exceptions ou le retour de message d'erreur ?
Oui, en Common Lisp notamment ou la gestion des erreurs va beaucoup plus loin et permet notamment la gestion des erreurs sans destruction de la pile d'appels. Il y a un article interessant à ce sujet sur wikipedia: http://fr.wikipedia.org/wiki/Syst%C3%A8me_de_gestion_d'excep(...)
[^]Re: Ma réponse :
Oui, PHP affiche un warning débile à l'écran (en espérant que quelqu'un le voie) et continue son exécution comme si de rien n'était.
[^]Re: Ma réponse :
ça depend comment tu l'a configuré.
[^]Re: Ma réponse :
Le principe des exceptions en soit n'est pas mauvais, c'est juste que ça reste très chiant à mettre en oeuvre (dans le sens où dans un projet relativement gros ça prends du temps, et qu'en général tu as autre chose en tête).
C'est quelque chose qu'il faut absolument prévoir dès le début.
Personnellement je conseille de créer au moins 2 types d'exceptions :
* exceptions de type fonctionnel : en cas d'impossibilité de conclure sur une règle de gestion, ou valeur non permise et qui risque de provoquer des erreurs graves plus loin.
* exceptions de type technique : pour signaler les problèmes i/o, connexion, timeout...
Avoir ces 2 classes permet de logger différement (2 fichiers, ou 1 fichier et 1 base de données) les 2 types de problèmes, gràce à la souplesse de log4j.
Ensuite suivant la complexité du projet il est utile de détailler plus finement la hierarchie des exceptions. Par exemple on peut très bien avoir envie de signaler dans une table de BDD des exceptions fonctionnelles corerspondant à une règle bien précise, et jeter tout le reste dans un fichier de log.
Typage des exceptions + LOG4J = ROXOR \o/
[^]Re: Ma réponse :
Je rajouterai que les exceptions techniques doivent proposer une "alternative" sous la forme d'un test. Exemple dans le framework .NET :
int Parse(string str) renvoi potentiellement une exception (à utiliser par défaut si on ne veut pas gérer le cas où la chaîne ne contient pas d'entier).
bool TryParse(string ref, out int value) qui retourne un booléen à la place d'une exception. Cela évite la construction "lourde" try/catch et surtout évite de lever une exception, ce qui peut être coûteux à l'exécution (faut chopper tout le contexte dans la pile des appels qui ont conduit à l'exception).
Bref l'exception doit rester exceptionnel.
MonoFrance
[^]Re: Ma réponse :
Attention à propos des exceptions, autant en Java et en C# ça pose pas de problèmes, autant en C++, ça empêche d'utiliser "-Bstatic" et ça impose le RTTI. Je sais pas comment fait Qt pour gérer les erreurs, mais ils y arrivent sans les exceptions, alors il doit y avoir une solution à ce problème satisfaisante.
[^]Exceptions et RTTI.
De quel compilateur s'agit-il ?
Avec Visual C++ (8.0), j'utilise les exceptions tout en ayant désactivé le RTTI. Quand j'utilise g++, je ne précise pas d'options ayant trait au RTTI, donc j'ignore si c'est activé ou non, ni même si cela existe sous g++.
Si on ne dispose pas des exceptions, on peut arriver à les simuler à l'aide de la bibliothèque C 'setjmp.h'. C'est peut-être ce qui est utilisé pour Qt ...
Projet Epeios (http://zeusw.org/epeios/) :
Bibliothèques C++ généralistes et dédiées.
[^]Re: Exceptions et RTTI.
tu as raison, pour g++, on peut à priori utiliser "-fno-rtti" avec des exceptions ... Le compilateur dont je parlais était Forte 6u1 et SS10, en mode "compat4".
[^]Re: Ma réponse :
> Je sais pas comment fait Qt pour gérer les erreurs
Il y a pas d'erreurs dans Qt. :-)
:-D !!!NOUVEAU!!!
[^]Re: Ma réponse :
Ah, les erreurs, c'est pas forcément les tiennes :)
Exemple 1: les erreurs de serveurs extérieurs.
Tu fais un appel XML-RPC à un serveur qui te renvoie une page HTML pour te dire "en cours de maintenance". (ça m'est arrivé plusieurs fois avec Wordpress.com). Boum, le parser XML !
Exemple 2: les erreurs de tes collègues
Ton collègue Pierre vient te voir: "Y'a un bug dans ton module dans ton module foo". Après une heure de recherche, tu te rends compte que Pierre a appelé ton module avec un nombre négatif, alors que ça n'a de sens qu'avec des nombres positifs. Si tu avais utilisé un assert(), le programme aurait crashé simplement avec un message clair que tu as écrit à l'attention des collègues qui utilisent mal ton code.
Alors à part ça, pour le premier exemple : à Flock on a défini une classe flockIError adapté aux erreurs qui viennent du serveur. Ça nous permet de mettre un code d'erreur serveur, un message d'erreur serveur, un code d'erreur Flock (unifié entre les services), et un message d'erreur Flock. Le message d'erreur Flock est localisé, comme ça on peut le montrer à l'utilisateur ("mauvais mot de passe", "serveur indisponible", "permissions insuffisante"...).
http://lxr.flock.com/source/flock/mozilla/flock/base/common/(...)
Parallèlement à ça, on a un logger qui a différents niveaux : erreur, warning, info, debug.
Pour le deuxième exemple, l'astuce c'est de mettre tout tes assert() dans du code de préprocesseur. Comme ça quand tu développes le moindre bug provoque un crash (c'est mieux pour débugguer) mais pour les releases tu changes les options de compilation pour désactiver les assert() et les erreurs ne provoquent plus de crash.
Un peu dans cette idée, pour Flock les exceptions Javascript (Flock est principalement développé en Javascript) qui ne sont pas capturées provoquent une boite de dialogue alert(). C'est très agaçant, et ça donne envie de réparer l'exception très vite :).
[^]Re: Ma réponse :
C'est là où on aimerait retrouver toutes les facilités d'eiffel pour ce genre de chose. (on se demande même pourquoi si peu de langages permettent d'implementer facilement des contrats : on arrive a peu pres à écrire des préconditions, mais pour les post-conditions, et les invariants de boucle c'est un peu tendu).
Le principe d'exception permet à mon avis de résoudre assez facilement le problème de signaler les erreurs. La difficulté je trouve c'est souvent quoi faire de l'exception ? Si on la catche tout de suite c'est presque aussi chiant que la gestion d'erreur classique. Si on le fait trop loin de ce qui l'a déclenché, c'est difficile de rattrapper le coup (que faire dans ce cas là à partir dire 'y'a une erreur').
Typiquement, une erreur d'allocation, on pourrait attendre deux secondes, faire le ménage dans le GC et retenter l'allocation mais ca sous-entend des choses assez lourdes derrière pour reprendre le fil de l'execution au bon endroit.
[^]Re: Ma réponse :
La programmation par contrat c'est bon, mangez en.
Ceci dit, dans des langages objet, leur utilisation en cas d'héritage + polymorphisme devient obscure. La syntaxe de Eiffel en témoigne (require else et consort). Pour autant, je ne vois pas en quoi c'est plus tendu pour les post-conditions et les invariants de boucle. Autant j'ai un problème avec les invariants de classe (on fait comment quand une instance de la classe a un invariant cassé puisqu'on ne peux plus invoquer de méthodes de cette instance sans rompre le contrat de l'invariant), autant je n'en ai pas plus avec les post-conditions (contrat de sortie) et les invariants de boucle (contrat local, a priori pour le debug?) qu'avec les pré-conditions (contrat de sortie).
[^]Re: Ma réponse :
La programmation pr contrats, c'est très bien mais insuffisant pour différents types d'erreurs:
- les erreurs inprédictibles. Typiquement d'entrée/sortie (la clef USB a été enlevée sauvagement, le réseau est coupé ...) et d'allocation mémoire
- les erreurs qui peuvent se déclencher selon des paramètres variables au cours du temps. Dans un système concurrent, comment s'assurer qu'un container ne soit pas plein ? Solution: une section critique
Pour les erreurs prédictibles, c'est l'idéal, sinon, il faut trouver une autre solution. Et la solution n'est pas forcément un crash du programme. Par exemple si le réseau est déconnecté, je veux un joli message d'erreur ... et je veux que l'application continue a s'exécuter (si elle a quelque chose à faire)
La Roue du Temps
[^]Re: Ma réponse :
En effet, alors je précise: la programmation par contrat c'est bien pour détecter les erreurs des programmeurs. Ça n'aide en rien à la résolution des erreurs externes genre de disque plein, de pb réseaux, etc.
[^]Re: Ma réponse :
Il y a aussi des erreurs ingérables que l'on arrive à gérer : http://www.beunited.org/bebook/The%20Kernel%20Kit/System.htm(...)
Alors l'excuse de la coupure d'alimentation cela me fait doucement rire!
[+] [^]Re: Ma réponse :
T'as oublié le smiley.
[^]Re: Ma réponse :
En effet, alors je précise: la programmation par contrat c'est bien pour détecter les erreurs des programmeurs.
Les erreurs simples d'utilisation d'une API peut-être. Les erreurs subtiles de sémantique, ça m'étonnerait, ou alors ça revient à réimplémenter une deuxième fois le programme sous forme de contrats (double-checking intégral).