Gestion des erreurs
La gestion des erreurs est un drôle d'oiseau. Il s'agit d'une chose importante,
dont on vante les vertus et à laquelle on doit porter attention, or dans
les notes de cours (incluant les miennes) on tend à la négliger
ou à l'escamoter, par souci de simplicité.
Quelques réflexions d'autres auteurs sur le sujet :
- Selon Michael Feathers en 2011, mieux vaut placer
le traitement des erreurs en périphérie du code (début
et fin des fonctions, par exemple) plutôt qu'en son coeur (je suis assez
d'accord avec lui; j'ai pris cette pratique de discussions avec mon éminent
collègue François
Jean) : http://michaelfeathers.typepad.com/michael_feathers_blog/2011/09/finessing-away-errors.html
- Comment gérer les erreurs avec délicatesse, un texte de Niklas
Frykholm en 2012 : http://altdevblogaday.com/2012/01/22/sensible-error-handling-part-1/
(j'aime bien qu'il exprime les choix en termes de prises de responsabilités
de la part des programmeuses et des programmeurs)
- Réflexions de James Hamilton en 2012 sur
les erreurs, leur gestion et la robustesse de manière générale :
http://perspectives.mvdirona.com/2012/02/26/ObservationsOnErrorsCorrectionsTrustOfDependentSystems.aspx
- En 2012, Andrzej Krzemieński discute du
traitement d'erreurs dans un programme C++
de moyenne envergure, surtout du point de vue fondamentalement risqué
du choix de « valeurs magiques » pour signaler les cas
d'erreurs : http://akrzemi1.wordpress.com/2012/06/19/beware-of-magic-values/
- Textes de Bruce Dawson sur l'importance de planter, idéalement le
plus tôt possible :
- De l'avis de Jesper Louis Andersen, en 2012,
le véhicule Curiosity, envoyé sur Mars, serait bâti dans
le respect des principes sous-jacents au code robuste que met de l'avant Erlang :
http://jlouisramblings.blogspot.ca/2012/08/getting-25-megalines-of-code-to-behave.html
- Texte de Raymond Chen, en 2009, sur ce qu'il
reste possible de faire quand un programme est véritablement corrompu :
http://blogs.msdn.com/b/oldnewthing/archive/2009/11/13/9921676.aspx
- Dans ce texte de 2012, Filip Ekberg émet
l'opinion que nous en sommes au point où nous nous attendons à
rencontrer des bogues, alors que nous devrions plutôt ne subir ces irritants
que de manière exceptionnelle :
http://blog.filipekberg.se/2012/09/27/lets-write-better-software/
- En 2012, Erik Akron (du moins, je pense que
c'est son nom) nous rappelle que, dans bien des cas, mieux vaut laisse un
programme planter : http://variadic.me/posts/2012-10-30-you-should-let-it-crash.html
- Selon Patrick Smacchia en 2013, un
avertissement devrait en général être traité comme une erreur :
http://www.codebetter.com/patricksmacchia/2013/07/04/code-rules-are-not-just-about-clean-code/
- Approches possibles pour la gestion des erreurs en
C++, texte de Jérémy
Cochoy en 2013 :
http://zenol.fr/site/2013/08/27/an-alternative-error-handling-strategy-for-cpp/
- Dans certains cas, il est difficile de déterminer a priori si un
compilateur devrait générer un avertissement ou une erreur. À cet effet, texte
d'Eric Lippert
en 2014 sur un cas subtil en
C# :
http://blog.coverity.com/2014/04/23/warnings-vs-errors/
- Comment se fait la gestion des erreurs avec
Ruby :
- Comment se fait la gestion des erreurs avec
Rust :
- Texte de 2015 par Michel Martens, portant sur l'erreur
humaine : http://soveran.com/human-error.html
- Texte d'intérêt historique (je n'ai pas la date de publication, mais on
parle visiblement du début des années '90) par
Sean Parent
sur les préconditions, les
postconditions et une formalisation de la
détection et du traitement des cas d'exceptions :
http://www.mactech.com/articles/develop/issue_11/Parent_final.html
- À propos de techniques pour détecter des erreurs dès la compilation et
pour empêcher les programmes risqués de compiler, texte d'Andrzej Krzemieński en 2015 :
https://akrzemi1.wordpress.com/2015/02/10/desired-compile-time-failures/
- Bien utiliser une fonction comme strerror() pour générer des messages d'erreurs, même dans une situation multiprogrammée.
Texte de Victor Zverovich en 2015 :
http://zverovich.net/2015/03/13/reliable-detection-of-strerror-variants.html
- Dans ce texte de 2015,
Raymond Chen
explique ce à quoi servait le bouton Ignore de
Windows 3.1
lors d'une erreur grave :
http://blogs.msdn.com/b/oldnewthing/archive/2015/07/17/10624299.aspx
- Selon Edaqua Mortoray en 2015, la partie la
plus complexe de la gestion des erreurs n'est pas l'erreur :
https://mortoray.com/2015/12/03/the-hard-part-of-error-handling-is-not-the-error-itself/
- Présentation de Sasha Goldshtein, portant sur l'automatisation de
l'analyse et du triage des problèmes :
https://www.dropbox.com/s/7uy6wats7ridd0g/CLRMD.pptx?dl=0
- Texte de 2016 par David Boike sur la gravité des
erreurs :
http://particular.net/blog/but-all-my-errors-are-severe
- Texte de 2016 par Huon Wilson, portant sur la
gravité des fuites de mémoire :
http://huonw.github.io/blog/2016/04/memory-leaks-are-memory-safe/
- En 2016, la série sur la gestion des erreurs
de Jonathan Müller parle entre autres d'exceptions :
- Texte de 2017 par « Mr. Anne Dev », portant sur les coûts bien réels des erreurs de
programmation :
https://medium.com/@mrannedev/how-software-mistakes-can-cost-real-users-real-money-60c8ceed0cfb
Assertions dynamiques
« Using assertions in code is a way of revealing assumptions, but perhaps not in the way most developers think.
Developers normally think in terms of the content of the assertion itself and a formal idea of documenting, in hard form, a constraint expected or required of the code. »
(Kevlin Henney,
source)
Une assertion est un état qu'une fonction tient pour acquis et qui doit
s'avérer pour que son exécution se poursuive. Les assertions dynamiques sont
les plus connues d'entre elles, mais il existe aussi des assertions statiques
dans certains langages (en particulier
C++).
Un exemple simple d'assertion dynamique serait :
#include <cassert>
#include <algorithm>
template <class T>
char* copie_brute(char *dest, std::size_t capacite, const T &val) {
using std::copy;
//
// une exception sera préférable si le programme doit
// être capable de récupérer; une assertion est préférable
// si le programme est dans un état tel que récupérer serait
// plus dangereux que terminer brusquement. Ici, on présume
// être dans un contexte scientifique
//
assert(sizeof(T)<=capacite);
auto p = reinterpret_cast<const char*>(&val);
copy(p, p + sizeof(T), dest);
return dest + sizeof(T);
}
Si vous vous demandez pourquoi j'ai utilisé std::copy
plutôt que std::memcpy() ici, voir
Traits et optimisation.
La prudence...
Mon grand ami Pez (Jean-François
Pezet) m'a raconté un incident vécu dans l'un de ses
milieux de travail il y a quelques années. Plusieurs compilateurs implémentent
assert() sous forme d'une macro qui disparaît lors de compilation
en mode production (Release), ce qui fait que les « invocations »
d'assert() sont alors conservées en période
de mise au point (Debug) et éliminées du code généré
pour livraison commerciale.
Conséquence : il est essentiel
de ne pas écrire de programmes dépendant d'opérations
effectuées dans une invocation de assert(),
ces opérations étant susceptibles de ne pas apparaître
dans la version finale du produit.
Ainsi, plutôt que...
// b n'est pas initialisé
int b;
// Initialiser b ET vérifier que getValue() retourne une valeur != 0
// (attention: ne fonctionne qu'en mode Debug!)
assert(b = getValue());
// En mode Release, b n'est pas encore initialisée
f(b);
...où l'appel f(b) mène à ce qu'on nomme du
Undefined Behavior, il faut prendre soin d'écrire...
// déclarer et initialiser b
int b = getValue();
// Vérifie que getValue() ait bien retourné une valeur != 0
assert(b);
// Tout est sous contrôle
f(b);
Lectures complémentaires
Résilience
La résilience d'un système, d'un programme ou d'un composant logiciel est un
terme général intégrant la robustesse et la tolérance aux pannes. Sous ce
chapeau se groupent plusieurs aspects de la gestion des erreurs :
- Détecter les risques de panne
- Mettre en place des mécanismes de redondance pour qu'un système poursuive
son opération malgré une panne
- Gérer gracieusement les pannes et les bris logiciels
- Réagir convenablement en fonction des situations problématiques, etc.
La résilience est une caractéristique architecturale et conceptuelle, qui
implique le matériel, l'encapsulation, le bon
design d'une
API, etc. Les assertions dynamiques font partie des mécanismes de
résilience d'un programme, tout comme les
assertions statiques,
les codes d'erreurs et les exceptions.
Ce que je fais, personnellement
Si un logiciel doit planter, mieux vaut s'assurer qu'il plante rapidement.
Dans un monde idéal :
- On détecterait les erreurs dès la compilation. Le compilateur
fait déjà une partie du travail, en rapportant les erreurs lexicales,
syntaxiques et une partie des erreurs sémantiques. Il est possible,
avec un langage dont le système
de types est suffisamment riche, de générer des assertions
statiques pour offrir des garanties sémantiques propres à
nos programmes. Profitons-en!
- Si une erreur nous échappe à la compilation, alors l'idéal est de la
saisir lors de l'édition des liens, avant que le code ne soit mis en
production
- On s'assurerait de planter en période de test, avec par exemple des
assertions dynamiques (assert()) pour tous les
cas irrécupérables, les erreurs de logique et autres problèmes qui dépendent
de la dynamique d'exécution du programme
- On réserverait les exceptions pour les
erreurs dont il est possible de récupérer mais qu'il vaut mieux ne pas ignorer
- Enfin, les codes d'erreurs tels que les valeurs particulières de
retour de fonctions et les consultations d'états d'erreurs globaux
au programme ou au thread courant (errno,
WSAGetLastError() et autres trucs du genre) devraient être un dernier
recours, pour les cas où il n'y a pas d'alternative ou pour les erreurs qui
peuvent être ignorées sans heurt
- Selon une autre perspective, mieux vaut saisir une erreur à la
construction d'un objet que lors de son utilisation
Notez que ces « consignes » constituent pour moi un guide
général, pas une loi à laquelle il faut obéir ou
un dogme auquel il faut adhérer à tout prix. Par exemple, les
exceptions ne peuvent pas toujours être utilisées en pratique,
du moins dans l'état actuel de nos connaissances, ayant un caractère
légèrement imprévisible qui les rend impropres (pour le
moment) dans les systèmes temps réel les plus stricts. Notez la
nuance : le traitement d'exceptions n'est pas lent, mais il est ardu de
déterminer a priori le temps maximal dont il aura besoin; le
problème en est un de prévisibilité, pas de vitesse.
Lectures complémentaires
Pour d'autres perspectives sur le sujet :