Aller au contenu principal
Retour au blogue

Développement logiciel

Comment gérer et réduire les exceptions

Samuel Beausoleil
11 mars 2021 ∙ 5 mins
Une femme regardant au loin sur un fond de couleurs froides

Voici la suite de notre série d’articles sous le thème de comment réparer la confiance face aux solutions numériques. Si vous n’avez pas encore lu le premier blogue de cette série, nous vous recommandons d'y jeter un coup d'oeil avant d'entreprendre votre lecture: Comment éviter les échecs de vos projets numériques.

Tous les logiciels ont des points sensibles où il est possible de prédire des problèmes. Que ce soit une recherche dans une liste ou la lecture d’un fichier, des problèmes peuvent survenir et il est important de les gérer. En gérant correctement les erreurs et exceptions (simplement « exceptions » pour la suite de ce texte), nous pouvons rendre plus résistants nos logiciels et ainsi les rendre plus fiables.

Gérer les exceptions

Try / Catch [/ Finally]

Try/Catch/Finally
Relevant XKCD: https://xkcd.com/1188/

La structure try/catch introduite par COBOL dans les années 60 était l’une des premières implémentations natives à un langage pour gérer les exceptions. Aujourd’hui encore, c’est la méthode la plus utilisée. C’est une excellente structure pour gérer les exceptions peu probables, et regrouper les actions suites à un problème survenu. Toutefois, elle n’est pas sans défaut. Elle couvre une large portion de code et de par sa structure n’invite pas à la résilience du code. C’est rare que l’erreur y soit gérée, la plupart du temps, elle est loggé puis relancée sans tentative d’en récupérer. Et même si l’on en récupère, le fait qu’elle couvre une zone de code plutôt qu’une opération spécifique peut rendre moins évident le besoin d’en récupérer ou la marche à suivre.

De plus, le flot non linéaire des programmes faisant usage d’exceptions en augmente leur complexité, surtout lorsque des exceptions doivent remonter plusieurs niveaux avant d’être gérés.

Result

Le Result (ou Résultat en français) est la réponse directe aux problèmes du try/catch. Un Result est le type de retour d’une méthode que l’on peut prédire avoir un risque raisonnable d’échouer et de pouvoir prédire la marche à suivre si une erreur se produit.

fn read_username_from_file() -> Result<String, io::Error> {
    let file: Result<File, io::Error> = File::open("user.txt");

    let mut file = match file {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut buffer = String::new();
    match file.read_to_string(&mut buffer) {
        Ok(_) => Ok(buffer),
        Err(e) => Err(e),
    }
}

fn main() {
    let username = match read_username_from_file() {
        Ok(name) => name,
        Err(_) => {
            println!("Couldn't read username file.");
            read_input("Please enter your username:")
        }
    };

    println!("Your username is {}.", username);
}

Comme démontré dans l’exemple en Rust précédent, l’utilisation de résultats permet de gérer de manière élégante, immédiate, et claire les exceptions prédictibles. On peut prévoir qu’un fichier X ne pourra pas être lu pour une multitude de raisons. Il est donc sage de prévoir quoi faire immédiatement et lisiblement si une exception survient. Dans le cas présent, demander à l’utilisateur d’entrer son nom manuellement.

Évidemment, un résultat n’est pas une balle d’argent pour 100% des cas d’exceptions, mais c’est un bon départ. Les exceptions traditionnelles avec Try/Catch peuvent remplir les autres types d’événements de manière plus générique.

Réduire les probabilités d’exceptions communes

Enfin, il est toujours préférable de réduire les risques d’exception lorsque possible. Beaucoup de manières de programmer augmentent le risque d’exception. Dans cette section, nous verrons divers éléments de programmation qui augmentent les risques d’exception et comment les éviter.

Null

L'infâme valeur null est l’ennemi juré des programmeurs qui ne sont pas habitués à son utilisation et qui doivent maintenant y faire face. Même pour les programmeurs aguerris, elle peut causer des soucis lorsque son apparition n’est pas prévue. L’inventeur de null, Tony Hoare, a lui-même déclaré que l’invention des pointeurs nulles était son « erreur d’un milliard de dollars ».

C’est pour ça que de plus en plus de nouvelles méthodes existent et sont encouragées afin de réduire les apparitions de cette valeur.

  • Dans les langages où il n’y a ni overloading, ni valeurs par défaut pour les paramètres de fonction optionnels, utiliser un type Option comme paramètre. Vous communiquez ainsi qu’un paramètre est optionnel et vous êtes rappelé de gérer le scénario où l’option est vide à chaque fois que vous travaillerez cette méthode.
  • De même, s’il se peut que votre fonction n’ai rien à retourner dans certains scénarios, retournez toujours une Option. Ainsi, ceux qui utilisent votre code ne seront jamais surpris par l’apparition d’un null.
  • Lorsqu’une collection est retournée et qu’il n’y a rien à retourner, retournez une collection vide. Toutes les implémentations de collections supportent d’être vides au moment d’un appel de méthode sur eux, ce qui permettra au programme de continuer naturellement.

Mémoire invalide

Les erreurs d’accès/écriture à des pointeurs invalides sont une autre source commune d’exceptions. C’est causé par une tentative d’accéder à une variable qui a déjà été libérée. Un concept important à appliquer est RAII. Évitez d’avoir des pointeurs bruts vers le heap dans du code d’exécution autant que possible. Ainsi, la vie d’un pointeur est toujours attachée à celle d’un objet qui en contrôle l’accès et dont la vie est automatiquement gérée par sa vie sur le stack.

Typer les variables

En indiquant clairement le type des variables à leur déclaration, on réduit ainsi les erreurs d’inattention et réduit les risques qu’un autre programmeur dans l’équipe appelle votre fonction avec les mauvaises informations. Ces problèmes seront ainsi attrapés au moment de la compilation.

Testez

Une suite de tests rigoureux avec une excellente couverture de code réduit les risques que des bogues vous glissent entre les doigts ou que des régressions apparaissent. Ne vous contentez pas que de tests unitaires, des tests d’intégration aussi sont utiles pour tester les ponts de communication entre les différents segments de votre programme.

Mot de fin

En tant que programmeur, il est de notre responsabilité de produire des programmes exacts et fiables. Trop souvent, nous ne payons que d’égards à la première partie de notre description de tâches. Il est temps de commencer à faire attention à leur fiabilité, et commencer à faire notre travail au complet.

Photo: ThisisEngineeringRaEng