Adloun

Erreurs, stockage et qualité du code

Cours · JavaScript : rendre le Web interactif (école d'ingénieur), chapitre 11 · école d'ingénieur, informatique · le début du chapitre, la suite sur Adloun

La Librairie du Campus charge maintenant ses livres depuis un serveur, et sait afficher un message quand le réseau la trahit. Il lui manque une chose que tout site marchand possède : un panier, qui se remplit depuis le catalogue et qui survit à la fermeture de l'onglet. Le panier introduit des règles (pas plus de vingt exemplaires, un total exact au centime) et des données que l'on conserve d'une visite à l'autre. C'est le moment de s'équiper pour écrire du code fiable.

Ce chapitre réunit quatre outils de qualité. Les exceptions signalent proprement ce qui ne va pas. Le stockage local du navigateur conserve des données, à condition de ne jamais lui faire aveuglément confiance. Le débogueur montre ce que fait réellement le programme, pas ce qu'on croit qu'il fait. Enfin, les tests automatiques et l'analyse statique (ESLint) trouvent les erreurs avant les utilisateurs. Le lecteur venu de Java retrouvera JUnit et les avertissements du compilateur, sous d'autres noms.

Objectifs du chapitre

  • Reconnaître les erreurs levées par le moteur (TypeError, ReferenceError, SyntaxError, RangeError) ; lever et rattraper ses propres erreurs, avec une classe dédiée et une cause.
  • Installer un dernier filet pour les erreurs que personne n'a rattrapées.
  • Conserver des données avec localStorage et sessionStorage, en JSON, en se protégeant des données corrompues et d'un stockage indisponible.
  • Déboguer avec la console, les points d'arrêt et l'exécution pas à pas.
  • Écrire et lancer des tests avec node:test ; remplacer une dépendance (stockage, réseau) par une doublure.
  • Faire relire son code par ESLint.

Les programmes du chapitre sont dans le dossier code/ch11 du livre.

11.1 Les exceptions

Le mécanisme est celui de Java : throw lève une exception, qui remonte la pile d'appels jusqu'au premier try dont le catch la rattrape ; si aucun ne la rattrape, le programme (ou, dans une page, la tâche en cours) s'arrête. Les différences tiennent en trois points.

Différences avec Java

  • Il n'y a pas d'exceptions vérifiées : aucune fonction ne déclare ce qu'elle peut lever (pas de throws), et rien n'oblige l'appelant à s'en occuper.
  • Un catch attrape tout : on ne peut pas écrire catch (RangeError e). On teste le type dans le bloc, avec instanceof.
  • On peut lever n'importe quelle valeur, une chaîne par exemple. Il ne faut pas le faire (section 11.1.3).

11.1.1 Les erreurs du moteur

Le moteur lève lui-même des objets d'erreur, dont la propriété name dit la nature et message le détail :


const essais = [
  () => null.titre,                  // propriété d'une valeur nulle
  () => prixTTC * 2,                 // variable jamais déclarée
  () => JSON.parse("{titre: 'Java'}"), // JSON invalide (guillemets simples, clé nue)
  () => (32).toFixed(200),           // argument hors de l'intervalle permis
  () => new Array(-1),               // taille négative
];

for (const essai of essais) {
  try {
    essai();
  } catch (e) {
    console.log(`${e.name.padEnd(14)} ${e instanceof Error} ${e.message}`);
  }
}

$ node erreurs/types-erreurs.mjs
TypeError      true Cannot read properties of null (reading 'titre')
ReferenceError true prixTTC is not defined
SyntaxError    true Expected property name or '}' in JSON at position 1 (line 1 column 2)
RangeError     true toFixed() digits argument must be between 0 and 100
RangeError     true Invalid array length
Définition 11.1Les erreurs prédéfinies
  • TypeError une valeur n'a pas le type attendu : propriété de null ou d'undefined, appel de ce qui n'est pas une fonction. De loin la plus fréquente.
  • ReferenceError un nom qui n'est pas déclaré (souvent une faute de frappe).
  • SyntaxError un texte qui n'est pas du code (ou du JSON) correct.
  • RangeError une valeur hors de l'intervalle permis.

Toutes héritent d'Error, qui porte aussi stack, la pile d'appels sous forme de texte.

Ces messages viennent du moteur V8 (Node et Chromium) ; Firefox et Safari formulent les mêmes erreurs autrement. Un programme ne compare donc jamais le texte d'un message, seulement le type.

11.1.2 finally

Comme en Java, le bloc finally s'exécute toujours, que le try se termine normalement, par un return ou par une exception :


function essayer(n) {
  try {
    console.log("  try", n);
    if (n === 2) throw new Error("deux");
    return `résultat ${n}`;
  } catch (e) {
    console.log("  catch", e.message);
    return "valeur de secours";
  } finally {
    console.log("  finally", n);
  }
}

console.log(essayer(1));
console.log(essayer(2));

$ node finally.mjs
  try 1
  finally 1
résultat 1
  try 2
  catch deux
  finally 2
valeur de secours

On y range ce qui doit être fait dans tous les cas : réactiver un bouton, retirer un indicateur de chargement (chapitres 9 et 10).

11.1.3 Lever ses propres erreurs

On lève toujours un objet Error, ou d'une classe qui en hérite. Une chaîne levée n'a ni nom, ni message, ni pile :


try {
  throw "stock épuisé";
} catch (e) {
  console.log(typeof e, e instanceof Error, e.message, e.stack !== undefined);
}
try {
  throw new Error("stock épuisé");
} catch (e) {
  console.log(typeof e, e instanceof Error, e.message, e.stack !== undefined);
}

$ node erreurs/lancer-chaine.mjs
string false undefined false
object true stock épuisé true

Une classe d'erreur propre à l'application (chapitre 6) permet à l'appelant de reconnaître ses erreurs, et de leur attacher des informations utiles, ici le champ en faute. L'option cause (ES2022) enveloppe une erreur de bas niveau dans une erreur plus parlante, sans perdre l'originale.


class ErreurValidation extends Error {
  constructor(champ, message, options) {
    super(message, options); // options : { cause }
    this.name = "ErreurValidation";
    this.champ = champ;
  }
}

function lireQuantite(texte) {
  const n = Number(texte);
  if (!Number.isInteger(n) || n < 1 || n > 20) {
    throw new ErreurValidation("quantite", `« ${texte} » n'est pas une quantité de 1 à 20`);
  }
  return n;
}

function lireCommande(json) {
  try {
    const donnees = JSON.parse(json);
    return { isbn: donnees.isbn, quantite: lireQuantite(donnees.quantite) };
  } catch (e) {
    if (e instanceof ErreurValidation) throw e;  // déjà parlante : on la laisse passer
    throw new Error("commande illisible", { cause: e }); // on l'enveloppe, sans la perdre
  }
}

for (const json of ['{"isbn":"A","quantite":"3"}', '{"isbn":"A","quantite":"trois"}', "{isbn:A}"]) {
  try {
    console.log("OK :", lireCommande(json));
  } catch (e) {
    console.log(`${e.name} : ${e.message}`);
    if (e.champ) console.log("  champ :", e.champ);
    if (e.cause) console.log("  cause :", e.cause.name, "—", e.cause.message);
  }
}

$ node lever.mjs
OK : { isbn: 'A', quantite: 3 }
ErreurValidation : « trois » n'est pas une quantité de 1 à 20
  champ : quantite
Error : commande illisible
  cause : SyntaxError — Expected property name or '}' in JSON at position 1 (line 1 column 2)

Méthode — que faire dans un catch ?

  • Ne rattraper que ce que l'on sait traiter à cet endroit : afficher un message, essayer autre chose, revenir à une valeur par défaut.
  • Tester le type (instanceof, e.name) et relever (throw e) ce qui n'était pas prévu : un TypeError inattendu est un bogue, pas une situation à masquer.
  • Ne jamais écrire un catch vide : une erreur avalée en silence est la plus difficile à trouver.

11.1.4 Le dernier filet

Dans une page, une erreur que personne ne rattrape interrompt la tâche en cours (le gestionnaire de clic, par exemple), s'affiche dans la console, et c'est tout : l'utilisateur voit un bouton qui « ne fait rien ». Deux événements de window permettent d'en être averti : error pour une exception non rattrapée, unhandledrejection pour une promesse rompue que personne n'attendait (chapitre 9).


function signaler(origine, erreur) {
  bandeau.hidden = false;
  const ligne = document.createElement("li");
  ligne.textContent = `${origine} — ${erreur?.name} : ${erreur?.message}`;
  journal.append(ligne);
}

window.addEventListener("error", (evenement) => signaler("error", evenement.error));
window.addEventListener("unhandledrejection", (evenement) => signaler("unhandledrejection", evenement.reason));

La page erreurs-globales/ a deux boutons fautifs : l'un lit une propriété d'undefined, l'autre lance un fetch vers un port sans serveur sans l'attendre. Après un clic sur chacun :

Illustration

La suite du cours est sur Adloun

Le chapitre en entier, formules et figures comprises, avec le tuteur, les fiches, les QCM et les exercices.

Lire la suite du cours

En accès libre, sans inscription.

Exercices corrigés de ce chapitre

Lire la suite du cours →