• [^] # Re: Vive les exceptions !

    Posté par (site web personnel) . En réponse au journal Qu'est-ce que bien gérer les erreurs dans ses programmes ?. Évalué à 1.

    Après avoir essayé divers approches, je suis retourné au mode "je teste les valeurs de retour" de toute mes fonctions.

    Pour moi, une gestion d'erreur propre, ca commence par de l'information. Avant de savoir ce qu'on va faire d'une erreur, on commence par donner un maximum d'information sur où elle s'est produite et pourquoi. Pour ça, le mieux est d'avoir un bon module de log, qui permet de partitionner les logs en domaines et en niveaux. Si j'ai trois couches d'API, je pouvoir activer ou désactiver le log de chacune des couches indépendamment, passer la couche basse en mode debug si c'est nécessaire. Ca permet de se lacher un terme de log dans les couches basses, sans pour autant inonder complètement les logs. Dans mes programmes à l'heure actuelle, j'ai un niveau de log "deepdebug". Par défaut, mes programmes sont en "debug". En mode "deepdebug", les couches basses sont très très bavardes et je sais tout ce qu'il se passe. Très très pratique.

    Le système de log doit à mon sens :
    - permettre à un client qui n'y connait rien de t'envoyer une trace si tu le lui demandes
    - être suffisamment détaillé et précis pour que tu puisses débugger ton programme sans lancer un debugger.

    Vu la qualité plutôt minable des divers debuggers sous Linux, c'est de toute façon intélligent de miser sur un bon système de log.

    Avec les logs, tu sais maintenant que tu pourras traiter correctement ton erreur a posteriori.

    Maintenant que faire de l'erreur quand elle se produit ? En regardant les programmes que j'ai écrit, je m'aperçois qu'il est exceptionnel qu'une erreur ne soit pas absolument bloquante. Dans mon cas, une erreur signifie que le programme ne peut plus s'exécuter. Donc le mieux est de remonter l'erreur vers les couches hautes pour en informer l'utilisateur et le laisser corriger le problème.

    Les exceptions permettent de s'affranchir du problème localement, en disant : "je traiterai cette erreur dans les couches hautes". Personnellement, je trouve que ce n'est pas une bonne approche parce que ça encourage à ne pas traiter le problème. C'est vite fait d'oublier dans les couches hautes qu'il pouvait y avoir l'exception X dans les couches basses. Ou bien on finit par attraper toutes les exceptions indifféremment mais alors le message de l'exception peut être cryptique.

    Exemple : tu reçois un IOError sur un programme avant même de toucher à un seul fichier dans un programme. Quel est le problème ? Impossible pour l'utilisateur de bien comprendre donc de résoudre.

    Problème réel: le programme utilise un système de log basé sur des fichiers mais il ne peut créer de nouveau fichier (la partition est pleine) donc il ne peut pas initialiser le système de log. Dans le cas de mes programmes, le message d'erreur sera plutôt "Could not initialise log system : file /tmp/toto.log could not be created."

    Je préfère traiter les erreurs localement parce que c'est là où on peut le mieux les décrire. Et je les gère avec des valeurs de retours. Tous mes programmes ou presque me conduisent à gérer mes propres valeurs et message d'erreur, que j'étends au fur à mesure des erreurs que je découvre. Les code d'erreurs doivent avoir un message correspondant. J'ai souvent des fonctions pour mapper les erreurs typiques de la couche en dessous vers des erreurs de ma propre lib d'erreur.

    Ca fait du code lourd à écrire mais ça fait du code robuste. J'ai horreur du code qui plante sans que tu saches ce qui s'est passé. Le pire, c'est le code qui plante loin dans le programme à cause d'une erreur qui s'est produite au début du programme. C'est typiquement le genre de code que tu mets des heures à debugger parce que tu cherches au mauvais endroit. S'il y a erreur, le programme doit s'interrompre et dire ce qui ne va pas.

    Mon code ressemble souvent à ca :


    jresult some_function( int a, int * b )
    {
    int ret;
    if (a < 0) {
    jlog_err( "my module", "a should > 0 but is %d", a );
    return JERR_INVALID;
    }

    if (b == NULL) {
    jlog_err( "my module", "b can not be NULL");
    return JERR_INVALID:
    }

    ret = some_sub_func( a, *b );
    if (ret != JOK) {
    jlog_err( "my module", "error when calling some_s_b_func");
    return ret;
    }

    ....

    return JOK;

    }


    Cela m'arrive aussi d'avoir des macros en C du type :

    #define TRY( some_call ) { ret = some_call ; if (ret != JOK) { jlog_err("my_module", "calling %s returned error %s", #some_call, error_msg( ret ) } }


    qui s'utilise avec :

    TRY( some_sub_func(a, *b) )


    C'est sur que c'est plus pratique avec des langages qui génère des exceptions et affichent toute la pile d'appel.

    Voila. On peut penser que c'est lourd et que ca fait perdre du temps d'écrire du code aussi verbeux mais je pense au contraire que ça fait gagner du temps car :
    - un programme vite fait finit toujours par durer beaucoup plus longtemps que prévu initialement. Mieux vaut l'écrire proprement du premier coup.
    - quand une erreur se produit, je sais tout de suite où et pourquoi. Et développer un programme, c'est bien passer son temps à comprendre pourquoi le ne réagit pas normalement.


    Un dernier point sur ce qu'on doit faire quand un programme plante réellement. A mon sens, on doit toujours essayer de sauver les meubles et de réduire l'impact du plantage pour l'utilisateur. Par exemple, j'ai utilisé longtemps le navigateur Opéra à une période où il plantait deux ou trois fois par jour. Ca m'est arrivé plein de fois de le voir planter avec 25 onglets ouverts. Horreur ! Sauf que quand je le relançais, je retrouvais mes 25 onglets. J'ai perdu 15 secondes à le relancer mais je n'ai à peine été dérangé. Il a fallu très très longtemps à firefox pour envisager de faire de même.

    Il doit y avoir pour chaque programme une réflexion sur ce qui est fondamental pour l'utilisateur de ne pas perdre et un mécanisme sur comment sauver cette information même dans les cas les plus critiques.

    C'est pour ça aussi que je suis plutôt contre les asserts. C'est un truc des fainéant et ça ne permet pas de sauver les meubles correctement.