Chapitre 5: Gestion des erreurs

Écrire des programmes qui fonctionnent quand tout se passe comme prévu, c’est un bon point de départ. Mais vous arranger pour que vos programmes se comportent de façon acceptable dans des circonstances inattendues, cela devient un véritable défi.

Les situations problématiques qu’un programme peut rencontrer se classent en deux catégories : les erreurs du développeur et les réels problèmes. Si quelqu’un oublie de passer un argument requis à une fonction, c’est un exemple de la première catégorie. En revanche, si un programme demande à l’utilisateur de saisir un nom et qu’il obtient en retour une chaîne vide, il s’agit d’un problème que le développeur ne peut pas empêcher.

En général, on traite les erreurs du développeur en les cherchant et en les corrigeant, et pour les erreurs réelles, en faisant en sorte que le code les vérifie et effectue l’action appropriée pour y remédier (par exemple en redemandant le nom de l’utilisateur), ou au moins en échouant de façon bien définie et propre.


Il est important de décider de quelle catégorie un certain problème peut relever. Par exemple, reprenons notre ancienne fonction puissance :

function puissance(base, exposant) {
  var resultat = 1;
  for (var compteur = 0; compteur < exposant; compteur++)
    resultat *= base;
  return resultat;
}

Quand un geek essaie d’appeler puissance("Lapin", 4), c’est de toute évidence une erreur du développeur, mais qu’en est-il de power(9, 0.5) ? La fonction ne sait pas manipuler des exposants sous forme de fraction, mais mathématiquement parlant, élever un nombre à la puissance 1/2 est parfaitement raisonnable (Math.pow sait le faire). Dans des situations où le type de saisie que peut accepter une fonction n’est pas totalement clair, il est préférable de préciser explicitement le type d’arguments acceptables dans un commentaire.


Si une fonction rencontre un problème qu’elle ne peut résoudre par elle-même, que doit-elle faire ? Dans le chapitre 4, nous avons écrit la fonction extraireChaineEntre :

function extraireChaineEntre(chaine, debut, fin) {
  var indexDebut = chaine.indexOf(debut) + debut.length;
  var indexFin = chaine.indexOf(fin, indexDebut);
  return chaine.slice(indexDebut, indexFin);
}

Si debut et fin donnés en argument n’apparaissent pas dans la chaîne, indexOf renverra -1 et cette version de extraireChaineEntre retournera des absurdités : extraireChaineEntre("Île déserte", "{-", "-}") renvoie "le désert".

Quand le programme s’exécute et que la fonction est appelée ainsi, le code qui l’a appelé obtiendra une chaîne, comme prévu, et continuera joyeusement à la manipuler. Mais la valeur est erronée, donc quel que soit le résultat obtenu, il sera faux. Et si vous êtes malchanceux, cette erreur ne provoquera de problème qu’après avoir été passée à une vingtaine d’autres fonctions. Dans des cas comme celui-ci, il est extrêmement difficile de trouver où le problème a débuté.

Dans certains cas, vous ne serez absolument pas concerné par ce genre de problème et vous n’aurez que faire du mauvais comportement de la fonction lorsqu’elle reçoit un mauvais type d’argument. Par exemple, si vous êtes sûr qu’une fonction ne sera appelée qu’à quelques endroits et que vous pouvez prouver que ces endroits ne fournissent que le bon type d’argument, ça ne vaut alors généralement pas le coup de faire grossir la fonction et de la rendre plus moche pour qu’elle puisse traiter des cas problématiques.

Mais la plupart du temps, les fonctions qui échouent « silencieusement » sont difficiles à utiliser, et même dangereuses. Que se passe-t-il si le code appelant extraireChaineEntre veut savoir si tout s’est bien passé ? Sur le moment, il ne peut le dire, sauf à refaire tout le travail qu’a effectué extraireChaineEntre et à vérifier le résultat de extraireChaineEntre par rapport au sien. Ce qui n’est pas terrible. Une solution serait de faire renvoyer par extraireChaineEntre une valeur spéciale telle que false ou undefined quand elle échoue.

function extraireChaineEntre(chaine, debut, fin) {
  var indexDebut = chaine.indexOf(debut);
  if (indexDebut == -1)
    return undefined;
  indexDebut += debut.length;
  var indexFin = chaine.indexOf(fin, indexDebut);
  if (indexFin == -1)
    return undefined;

  return chaine.slice(indexDebut, indexFin);
}

Vous pouvez voir que les vérifications d’erreurs ne rendent généralement pas les fonctions plus jolies. Mais maintenant, le code qui appelle extraireChaineEntre peut faire quelque chose comme :

var saisie = prompt("Dites-moi quelque chose", "");
var entreParentheses = extraireChaineEntre(saisie, "(", ")");
if (entreParentheses != undefined)
  print("Vous avez mis entre parenthèses '", entreParentheses, "'.");

Dans beaucoup de cas, renvoyer une valeur spéciale est une façon tout à fait appropriée pour indiquer une erreur. Il y a malheureusement un revers à la médaille. D’abord, que se passe-t-il si la fonction peut déjà renvoyer toutes sortes de valeurs possibles ? Par exemple, prenons cette fonction qui récupère le dernier élément d’un tableau :

function dernierElement(tableau) {
  if (tableau.length > 0)
    return tableau[tableau.length - 1];
  else
    return undefined;
}

show(dernierElement([1, 2, undefined]));

Le tableau avait-il un dernier élément ? En regardant la valeur que renvoie dernierElement, c’est impossible à dire. Le second problème quand on renvoie des valeurs spéciales, c’est que cela peut conduire à créer pas mal de bazar. Si une partie de code appelle extraireChaineEntre dix fois, elle doit vérifier dix fois si undefined a été retourné. De même, si une fonction appelle extraireChaineEntre, mais n’a pas de stratégie pour gérer un éventuel échec, elle devra vérifier la valeur renvoyée par extraireChaineEntre, et si c’est undefined, cette fonction peut alors renvoyer undefined ou une autre valeur spéciale à sa fonction appelante, qui à son tour vérifiera cette valeur.

Parfois, quand quelque chose de bizarre se passe, il serait pratique d’arrêter ce que l’on est en train de faire, et de revenir immédiatement à un endroit où le problème peut être réglé.

Nous avons de la chance. Beaucoup de langages de programmation fournissent de tels mécanismes. C’est ce qu’on appelle généralement la gestion des exceptions.


La théorie derrière la gestion des exceptions fonctionne ainsi : il est possible pour le code de lever (ou lancer) une exception, qui est une valeur. Quand on lève une exception, cela ressemble parfois à un retour de fonction boosté aux stéroïdes : on ne sort pas simplement de la fonction en cours, mais aussi des fonctions appelantes, en retournant jusqu’au niveau qui a démarré l’exécution actuelle. Cela s’appelle dépiler. Vous vous rappelez peut-être la pile des appels de fonction qui avait été abordée au chapitre 3. Une exception descend dans cette pile, en renvoyant tous les contextes des appels qu’elle rencontre.

Si elles descendaient sans s’arrêter jusqu’au bas de la pile, les exceptions ne seraient pas d’un grand intérêt, elles fourniraient juste un moyen original de détruire le programme. Heureusement, il est possible de dresser des obstacles aux exceptions le long de la pile. Ceux-ci « interceptent » l’exception quand elle descend, et ils peuvent la prendre en charge, après quoi le programme continue de fonctionner normalement à partir du point où l’exception a été attrapée.

Un exemple :

function dernierElement(tableau) {
  if (tableau.length > 0)
    return tableau[tableau.length - 1];
  else
    throw "Impossible de prendre le dernier élément d’un tableau vide.";
}

function dernierElementPlusDix(tableau) {
  return dernierElement(tableau) + 10;
}

try {
  print(dernierElementPlusDix([]));
}
catch (erreur) {
  print("Une erreur est survenue : ", erreur);
}

throw est le mot-clé qui est utilisé pour lever l’exception. Le mot-clé try pose un obstacle pour les exceptions : quand une exception est levée dans le code du bloc suivant ce try, le bloc catch sera exécuté. La variable nommée entre parenthèses après le mot catch est le nom donné à la valeur d’exception à l’intérieur du bloc.

On remarque que la fonction dernierElementPlusDix ignore complètement la possibilité que dernierElement ne fonctionne pas. C’est là le grand avantage des exceptions, un code pour s’occuper de l’erreur n’est nécessaire qu’au moment où l’erreur survient, et à l’endroit où on s’en occupe. Les fonctions sur le chemin peuvent tout ignorer à ce sujet.

Enfin, presque.


Réfléchissez un instant à ceci : une fonction faireDesTrucs veut déclarer une variable globale trucEnCours pour pointer vers quelque chose de spécifique pendant que son corps exécute, de manière à ce que d’autres fonctions puissent également y avoir accès. Normalement, vous passeriez simplement cette chose comme un argument, mais imaginons l’espace d’un instant que ce n’est pas possible en pratique. Quand la fonction se termine, trucEnCours devrait être redéfinie avec une valeur null.

var trucEnCours = null;

function faireDesTrucs(unTruc) {
  if (trucEnCours != null)
    throw "Oh non ! Nous sommes déjà en train d’exécuter quelque chose !";

  trucEnCours = unTruc;
  /* faire des choses compliqués… */
  trucEnCours = null;
}

Mais que ce se passerait-il si cette opération compliquée lève une exception ? Dans ce cas, l’appel à faireDesTrucs sera rejeté en dehors de la pile par l’exception, et trucEnCours n’aura pas de valeur redéfinie comme null.

Les instructions try peuvent aussi être suivies par un mot-clé finally, ce qui veut dire « quoi qu’il arrive, exécutez ce code après avoir essayé d’exécuter ce code dans un bloc try ». Si une fonction doit nettoyer quelque chose, le code qui effectue ce nettoyage doit en général être inséré dans un bloc finally :

function faireDesTrucs(unTruc) {
  if (trucEnCours != null)
    throw "Oh non ! Nous sommes déjà en train d’exécuter quelque chose !";

  trucEnCours = unTruc;
  try {
    /* faire des choses compliqués… */
  }
  finally {
    trucEnCours = null;
  }
}

Beaucoup d’erreurs de programmation obligent l’environnement JavaScript à lever des exceptions. Par exemple :

try {
  print(Yeti);
}
catch (erreur) {
  print("Intercepté : " + erreur.message);
}

Dans des cas comme celui-là, des objets spéciaux de type erreur sont levés. Ils ont toujours une propriété message contenant une description du problème. Vous pouvez lever des objets similaires en utilisant le mot-clé new et le constructeur error :

throw new Error("Au feu !");

Quand une exception descend tout en bas de la pile sans être traitée, elle est prise en charge par l’environnement. Ce que cela signifie diffère selon les différents navigateurs, quelquefois une description de l’erreur est écrite sous la forme d’une entrée de journal, d’autres fois une fenêtre décrivant l’erreur apparaît.

Les erreurs générées par le code entré dans la console sur cette page sont toujours attrapées par la console, et sont affichées avec les autres sorties de la console.


La plupart des programmeurs considèrent les exceptions uniquement comme un mécanisme de gestion des erreurs. Par essence, pourtant, elles représentent seulement une autre manière d’influer sur le contrôle du flux d’un programme. Par exemple, elles peuvent être utilisées comme une sorte d’instruction break dans une fonction récursive. Voici une fonction un peu bizarre qui détermine si un objet, ainsi que les autres objets stockés à l’intérieur, contiennent au moins sept valeurs true :

var SeptValeursTrue = {};

function contientSeptValeursTrue(objet) {
  var compte = 0;

  function compter(objet) {
    for (var nom in objet) {
      if (objet[nom] === true) {
        compte++;
        if (compte == 7)
          throw SeptValeursTrue;
      }
      else if (typeof objet[nom] == "object") {
        compter(objet[nom]);
      }
    }
  }

  try {
    compter(objet);
    return false;
  }
  catch (exception) {
    if (exception != SeptValeursTrue)
      throw exception;
    return true;
  }
}

La fonction interne compter est appelée récursivement pour chaque objet qui fait partie d’un argument. Quand la variable compte atteint sept, il n’y a aucun intérêt à continuer de compter, mais se contenter de remonter de l’appel courant à compter ne va pas nécessairement arrêter l’énumération, car il pourrait y avoir plusieurs appels derrière. Donc ce que l’on fait c’est juste lever une exception, ce qui obligera le contrôleur à rejeter tout appel, et à se rendre au bloc catch.

Mais se contenter de retourner true dans le cas d’une exception n’est pas correct. Quelque chose peut mal se passer, donc on vérifie d’abord si l’exception est l’objet SeptValeursTrue, créé spécifiquement dans ce but. Si ce n’est pas le cas, ce bloc catch ne sait pas comment s’en occuper, donc il la lève encore.

On a ici un modèle qui est également habituel lorsqu’on s’occupe de conditions d’erreur : vous devez vous assurez que votre bloc catch s’occupe seulement des exceptions qu’il sait traiter. Lever des exceptions de type chaîne de caractères, comme certains exemples de ce chapitre le font, est rarement une bonne idée, car cela rend difficile de reconnaître le type de l’exception. Une meilleure idée consiste à utiliser des valeurs uniques, comme l’objet SeptValeursTrue, ou d’introduire un nouveau type d’objets, comme décrit dans le chapitre 8.