5.3 · Exceptions

Les diapositives de la séance 5

  • lever une exception avec throw et la rattraper avec try / catch / finally
  • expliquer pourquoi la variable de catch est de type unknown et la traiter proprement
  • créer ses propres classes d’erreur héritant de Error
  • distinguer plusieurs erreurs avec instanceof et ordonner correctement les tests
  • décider quand une exception est justifiée et quand elle ne l’est pas

L’héritage de classes et instanceof, vus au chapitre 3.2 · Contrats, héritage et polymorphisme.

Code du chapitre : télécharger app.ts

Pourquoi des exceptions ?

En C, une fonction qui échoue renvoie un code d’erreur, et l’appelant est censé le tester. En pratique, il oublie, et l’erreur se propage silencieusement jusqu’au plantage.

Les exceptions inversent la logique : une erreur interrompt le flux normal d’exécution et remonte automatiquement la pile d’appels jusqu’à ce que quelqu’un la traite. Le bénéfice principal est la séparation : le code qui calcule reste lisible, le code qui gère les erreurs est regroupé ailleurs.

// Style C : le chemin nominal est noyé dans les tests
const a = lire();
if (a === null) return -1;
const b = convertir(a);
if (b === null) return -2;

// Style exception : le chemin nominal est visible d'un coup d'œil
try {
  const b = convertir(lire());
} catch (e) {
  // tout le traitement d'erreur est ici
}

throw : lever une exception

app.ts
class Calculatrice {
  diviser(a: number, b: number): number {
    if (b === 0) {
      throw new Error("Division par zéro n'est pas autorisée");
    }
    return a / b;
  }
}

throw interrompt immédiatement la fonction. Les lignes suivantes ne s’exécutent pas, et la fonction ne renvoie rien : le moteur remonte à l’appelant, puis à l’appelant de l’appelant, jusqu’à trouver un try. S’il n’en trouve aucun, le programme s’arrête avec la trace de la pile.

Techniquement, JavaScript permet de lever n’importe quoi : throw "oups"; ou throw 42; sont valides. Ne le faites jamais. Vous perdez le message structuré, le nom de l’erreur et surtout la pile d’appels (stack), qui est ce qui vous permettra de localiser le problème. Levez toujours un Error ou une classe qui en hérite.

try / catch / finally

app.ts
class Convertisseur {
  convertirEnNombre(input: string): number {
    const resultat = Number(input);
    if (isNaN(resultat)) {
      throw new Error(`'${input}' ne peut pas être converti en nombre`);
    }
    return resultat;
  }

  essayerConversion() {
    try {
      console.log(this.convertirEnNombre("123"));   // affiche 123
      console.log(this.convertirEnNombre("abc"));   // lève : la suite du try est ignorée
    } catch (e) {
      if (e instanceof Error) {
        console.error(e.message);
      }
    } finally {
      console.log("Fin de la tentative de conversion.");
    }
  }
}

Sortie :

123
'abc' ne peut pas être converti en nombre
Fin de la tentative de conversion.

Trois blocs, trois rôles :

Bloc Rôle Obligatoire
try contient le code susceptible d’échouer oui
catch reçoit l’erreur et la traite l’un des deux
finally s’exécute dans tous les cas l’un des deux

finally s’exécute que le try réussisse, échoue, ou même exécute un return. C’est l’endroit où l’on libère des ressources : fermer un fichier, arrêter un indicateur de chargement, rétablir un état.

NoteUn seul catch, pas plusieurs

Contrairement à Java ou C++, JavaScript n’a pas de catch multiples par type. Il n’y a qu’un seul bloc catch, dans lequel on distingue les cas soi-même avec instanceof.

La variable de catch est unknown

C’est le point que TypeScript traite différemment de JavaScript, et il découle du piège précédent : puisque n’importe quoi peut être levé, TypeScript refuse de supposer que vous avez reçu un Error.

try {
  risque();
} catch (e) {
  console.error(e.message);
  // ERREUR : 'e' is of type 'unknown'
}

Le traitement correct passe par une vérification :

try {
  risque();
} catch (e: unknown) {
  if (e instanceof Error) {
    console.error(e.message);   // ici TypeScript sait que e est un Error
  } else {
    console.error("Erreur non standard :", e);
  }
}

La tentation est d’écrire catch (e: any) ou catch (e) { (e as Error).message }. Les deux compilent. Les deux replantent si le code appelé lève autre chose qu’un Error, et ce sera au moment où vous essayez justement de comprendre un bug. Prenez l’habitude du if (e instanceof Error) : il fait trois lignes et il est toujours correct.

L’objet Error porte trois propriétés utiles :

const e = new Error("quelque chose a mal tourné");
console.log(e.message);   // "quelque chose a mal tourné"
console.log(e.name);      // "Error"
console.log(e.stack);     // la pile d'appels, pour le débogage

Créer ses propres classes d’erreur

NoteRappel POO

Une classe d’erreur hérite de Error : extends Error, puis super(message) avant tout accès à this dans le constructeur. instanceof reconnaît ensuite chaque sorte d’erreur. Voir 3.2 · L’héritage : extends et super et 3.2 · Le bénéfice réel pour instanceof.

Quand un programme peut échouer de plusieurs façons, Error seul ne suffit plus : il faudrait comparer des chaînes de messages, ce qui est fragile. On crée alors une classe par type d’erreur, héritant de Error.

app.ts
class ErreurDivisionParZero extends Error {
  constructor() {
    super("Division par zéro n'est pas autorisée");   // message transmis à Error
    this.name = "ErreurDivisionParZero";              // utile dans les traces
  }
}

class ErreurValeurTropGrande extends Error {
  valeur: number;                    // donnée supplémentaire propre à cette erreur

  constructor(valeur: number) {
    super(`La valeur ${valeur} dépasse 100`);
    this.name = "ErreurValeurTropGrande";
    this.valeur = valeur;
  }
}

Trois points, tous déjà vus au chapitre 3.2 :

  • extends Error : ces classes sont des Error, elles en héritent message, name et stack ;
  • super(...) transmet le message au constructeur parent, en première instruction ;
  • une classe d’erreur peut porter des données (valeur), et c’est tout son intérêt : le bloc catch pourra les exploiter au lieu de les extraire d’un message texte.

Distinguer les erreurs avec instanceof

app.ts
class CalculatriceAvancee {
  diviser(a: number, b: number): number {
    if (a > 100) throw new ErreurValeurTropGrande(a);
    if (b > 100) throw new ErreurValeurTropGrande(b);
    if (b === 0) throw new ErreurDivisionParZero();
    return a / b;
  }

  essayerDivision(a: number, b: number) {
    try {
      console.log(`${a} / ${b} = ${this.diviser(a, b)}`);
    } catch (e: unknown) {
      if (e instanceof ErreurDivisionParZero) {
        console.error("Erreur attrapée : division par zéro !");
      } else if (e instanceof ErreurValeurTropGrande) {
        console.error(`Erreur attrapée : ${e.valeur} est trop grand (max 100)`);
      } else if (e instanceof Error) {
        console.error("Erreur inattendue :", e.message);
      }
    } finally {
      console.log("Fin de la tentative de division.");
    }
  }
}

const calc = new CalculatriceAvancee();
calc.essayerDivision(10, 2);    // 10 / 2 = 5
calc.essayerDivision(10, 0);    // Erreur attrapée : division par zéro !
calc.essayerDivision(200, 5);   // Erreur attrapée : 200 est trop grand (max 100)

Remarquez e.valeur dans la deuxième branche : après le test instanceof, TypeScript réduit le type de e à ErreurValeurTropGrande et vous donne accès à ses propriétés spécifiques. C’est le même mécanisme que le type guard du chapitre 5.1 · JSON.

L’ordre des tests est déterminant. ErreurDivisionParZero hérite de Error, donc e instanceof Error est vrai pour elle aussi. Si vous testez Error en premier, vous entrez toujours dans cette branche et les cas spécifiques ne sont jamais atteints.

Règle : du plus spécifique au plus général, la classe la plus générale en dernier.

Quand ne PAS utiliser d’exception

Les exceptions sont coûteuses à lire et à suivre. Elles sont faites pour l’exceptionnel, pas pour le prévisible.

Situation Exception ? À faire à la place
Argument invalide passé à une fonction publique oui
Fichier introuvable, réseau coupé oui
Invariant de classe violé (bug de programmation) oui
Recherche qui ne trouve rien non renvoyer T | undefined
Champ de formulaire vide non renvoyer un message de validation
Contrôler une boucle, sortir d’une fonction non return, break
// Mauvais : l'absence de résultat est un cas NORMAL
function trouver(id: string): User {
  const u = users.find((x) => x.id === id);
  if (!u) throw new Error("introuvable");
  return u;
}

// Bon : le type dit la vérité, et le compilateur force l'appelant à traiter le cas
function trouver(id: string): User | undefined {
  return users.find((x) => x.id === id);
}

La deuxième version est meilleure pour une raison précise : User | undefined est vérifié par le compilateur, alors qu’une exception ne figure nulle part dans la signature et peut être oubliée sans avertissement.

Deux autres règles de conduite :

  • Ne rattrapez pas une erreur que vous ne savez pas traiter. Un catch vide (catch (e) {}) est la pire ligne de code possible : le programme continue dans un état incohérent, et vous n’avez plus aucune trace.
  • Rattrapez là où vous pouvez décider. Souvent, la bonne place est la couche haute (l’interface, le gestionnaire de route), pas la fonction qui a détecté le problème.

Exercices

Exercice 5.7 : Lire l’ordre d’exécution

essentiel

Dans exercice09a.ts, écrivez :

function test(x: number): string {
  try {
    console.log("A");
    if (x < 0) throw new Error("négatif");
    console.log("B");
    return "retour du try";
  } catch (e) {
    console.log("C");
    return "retour du catch";
  } finally {
    console.log("D");
  }
}

console.log(test(5));
console.log(test(-1));
  1. avant d’exécuter, écrivez sur papier la sortie attendue pour chaque appel ;
  2. exécutez et comparez ;
  3. remplacez le contenu du finally par return "retour du finally"; et recommencez. Que constatez-vous ? Pourquoi est-ce une très mauvaise idée ?

Sortie complète du programme :

A
B
D
retour du try
A
C
D
retour du catch

Ligne par ligne, pour test(5) :

Affichage Ce qui se passe
A on entre dans le try, premier console.log
x < 0 est faux : pas de throw, le catch ne sera jamais atteint
B deuxième console.log du try
return "retour du try" : la valeur est calculée et mise de côté, mais la fonction ne rend pas encore la main
D le finally s’exécute avant que le return ne prenne effet
retour du try la valeur mise de côté remonte enfin, et le console.log extérieur l’affiche

Puis pour test(-1) :

Affichage Ce qui se passe
A on entre dans le try
x < 0 est vrai : throw interrompt immédiatement le try
console.log("B") n’est jamais exécuté, ni le return du try
C le catch reçoit l’erreur
return "retour du catch" : même mécanisme, valeur mise de côté
D le finally s’exécute, exactement comme dans le cas nominal
retour du catch la valeur remonte

Le point clé. Les deux surprises classiques sont au même endroit : D s’affiche avant la valeur de retour, dans les deux appels. C’est la définition même de finally : il s’exécute après que le sort du try est scellé (succès, exception ou return) mais avant que la fonction ne rende la main. La plupart des étudiants prédisent A B retour du try D, en supposant que return sort immédiatement. Il ne sort pas : il déclenche d’abord le finally.

Deuxième surprise : B n’apparaît pas du tout dans le second appel. throw ne « signale pas une erreur », il interrompt le flux. Toutes les lignes restantes du try sont abandonnées.

function test(x: number): string {
  try {
    console.log("A");
    if (x < 0) throw new Error("négatif");
    console.log("B");
    return "retour du try";
  } catch (e) {
    console.log("C");
    return "retour du catch";
  } finally {
    console.log("D");
    return "retour du finally";   // à ne JAMAIS écrire
  }
}

Nouvelle sortie :

A
B
D
retour du finally
A
C
D
retour du finally

Les deux return précédents ont disparu. Le return du finally écrase la valeur mise de côté par le try comme par le catch : quoi qu’il arrive dans la fonction, elle renvoie toujours la même chose.

Pourquoi c’est une très mauvaise idée. Écraser une valeur de retour est déjà gênant, mais le vrai danger est ailleurs : un return dans un finally avale aussi les exceptions.

function avale(): string {
  try {
    throw new Error("la base de données est inaccessible");
  } finally {
    return "tout va bien";   // l'exception disparaît, définitivement
  }
}

console.log(avale());   // tout va bien

Il n’y a pas de catch ici, donc l’erreur devrait remonter à l’appelant. Elle est supprimée par le return du finally : aucune trace, aucun message, aucun moyen de savoir que quelque chose a échoué. C’est la même faute qu’un catch (e) {} vide, en plus insidieux, parce qu’elle ne ressemble pas à de la gestion d’erreur.

La règle est donc absolue : un finally ne contient que des instructions de nettoyage : jamais de return, jamais de throw, jamais de break ni de continue, qui ont tous le même effet d’écrasement. La règle ESLint no-unsafe-finally existe uniquement pour détecter ce cas.

Exercice 5.8 : Rectangle

essentiel

Dans exercice09b.ts :

  1. créez une classe Rectangle avec longueur et largeur, initialisées par le constructeur ;
  2. ajoutez calculerSurface(): number qui lève une Error si longueur ou largeur est négative ou nulle, et le message doit indiquer quelle dimension pose problème ;
  3. ajoutez essayerCalculSurface(): void qui appelle calculerSurface() dans un try / catch / finally, affiche la surface ou le message d’erreur, et signale la fin du traitement dans le finally ;
  4. créez trois instances : une valide, une à largeur nulle, une à longueur négative, et appelez essayerCalculSurface() sur chacune.

Critères de réussite : les trois appels s’exécutent sans interrompre le programme, le catch utilise instanceof Error, et aucun any n’apparaît dans le fichier.

class Rectangle {
  constructor(public longueur: number, public largeur: number) {}

  calculerSurface(): number {
    if (this.longueur <= 0) {
      throw new Error(`Longueur invalide : ${this.longueur}`);
    }
    if (this.largeur <= 0) {
      throw new Error(`Largeur invalide : ${this.largeur}`);
    }
    return this.longueur * this.largeur;
  }

  essayerCalculSurface(): void {
    try {
      const surface = this.calculerSurface();
      console.log(
        `Surface du rectangle ${this.longueur} x ${this.largeur} : ${surface}`
      );
    } catch (e: unknown) {
      if (e instanceof Error) {
        console.error(`Erreur : ${e.message}`);
      } else {
        console.error("Erreur non standard :", e);
      }
    } finally {
      console.log("Fin de la tentative de calcul.");
    }
  }
}

const valide = new Rectangle(5, 4);
const largeurNulle = new Rectangle(6, 0);
const longueurNegative = new Rectangle(-2, 4);

valide.essayerCalculSurface();
largeurNulle.essayerCalculSurface();
longueurNegative.essayerCalculSurface();

Sortie :

Surface du rectangle 5 x 4 : 20
Fin de la tentative de calcul.
Erreur : Largeur invalide : 0
Fin de la tentative de calcul.
Erreur : Longueur invalide : -2
Fin de la tentative de calcul.

Le point clé. L’exception est levée dans calculerSurface et rattrapée dans essayerCalculSurface, qui est une autre méthode. C’est toute la valeur des exceptions : la méthode qui détecte le problème n’a pas à savoir quoi en faire, et la méthode qui décide n’a pas à tester un code de retour. Remarquez aussi que calculerSurface garde un type de retour number, pas number | null : le chemin nominal reste propre.

Comme chaque appel a son propre try, une erreur n’interrompt pas le programme : les trois rectangles sont traités, et le finally s’exécute dans les trois cas.

L’erreur classique de l’exercice est d’écrire directement :

} catch (e) {
  console.error(e.message);   // ERREUR : 'e' is of type 'unknown'.
}

Et ses deux fausses corrections, catch (e: any) et (e as Error).message, qui font taire le compilateur sans rien vérifier : si le code appelé finit un jour par lever autre chose qu’un Error, elles produisent undefined ou plantent. Le if (e instanceof Error) coûte trois lignes et est toujours correct.

Notez enfin l’ordre des tests dans calculerSurface : la longueur est vérifiée en premier, donc un rectangle (-2, 0) signalerait uniquement la longueur. C’est acceptable ici : signaler toutes les erreurs d’un coup demanderait d’accumuler les problèmes dans un tableau plutôt que de lever à la première occasion.

Exercice 5.9 : Erreurs personnalisées

entraînement transfert

Reprenez l’exercice 5.8 et remplacez l’Error générique par deux classes d’erreur :

  1. ErreurDimensionNegative extends Error, portant une propriété dimension: "longueur" | "largeur" et la valeur: number fautive ;
  2. ErreurDimensionNulle extends Error, portant la même propriété dimension ;
  3. adaptez calculerSurface() pour lever la bonne erreur selon le cas ;
  4. dans le catch, distinguez les trois cas (ErreurDimensionNegative, ErreurDimensionNulle, puis Error) et affichez un message différent utilisant les propriétés des erreurs, jamais leur message texte ;
  5. inversez volontairement l’ordre des tests en plaçant e instanceof Error en premier, exécutez, et notez ce qui change.

Critère de réussite : à l’étape 4, e.dimension est accessible sans aucun as, c’est le signe que le rétrécissement de type fonctionne.

type Dimension = "longueur" | "largeur";

class ErreurDimensionNegative extends Error {
  constructor(public dimension: Dimension, public valeur: number) {
    super(`${dimension} négative : ${valeur}`);
    this.name = "ErreurDimensionNegative";
  }
}

class ErreurDimensionNulle extends Error {
  constructor(public dimension: Dimension) {
    super(`${dimension} nulle`);
    this.name = "ErreurDimensionNulle";
  }
}

class Rectangle {
  constructor(public longueur: number, public largeur: number) {}

  calculerSurface(): number {
    if (this.longueur < 0) {
      throw new ErreurDimensionNegative("longueur", this.longueur);
    }
    if (this.largeur < 0) {
      throw new ErreurDimensionNegative("largeur", this.largeur);
    }
    if (this.longueur === 0) throw new ErreurDimensionNulle("longueur");
    if (this.largeur === 0) throw new ErreurDimensionNulle("largeur");

    return this.longueur * this.largeur;
  }

  essayerCalculSurface(): void {
    try {
      console.log(
        `Surface du rectangle ${this.longueur} x ${this.largeur} : ${this.calculerSurface()}`
      );
    } catch (e: unknown) {
      // du plus SPÉCIFIQUE au plus GÉNÉRAL
      if (e instanceof ErreurDimensionNegative) {
        console.error(
          `La ${e.dimension} vaut ${e.valeur} : une dimension ne peut pas être négative.`
        );
      } else if (e instanceof ErreurDimensionNulle) {
        console.error(`La ${e.dimension} est nulle : la surface serait nulle.`);
      } else if (e instanceof Error) {
        console.error("Erreur inattendue :", e.message);
      } else {
        console.error("Erreur non standard :", e);
      }
    } finally {
      console.log("Fin de la tentative de calcul.");
    }
  }
}

new Rectangle(5, 4).essayerCalculSurface();
new Rectangle(6, 0).essayerCalculSurface();
new Rectangle(-2, 4).essayerCalculSurface();

Sortie :

Surface du rectangle 5 x 4 : 20
Fin de la tentative de calcul.
La largeur est nulle : la surface serait nulle.
Fin de la tentative de calcul.
La longueur vaut -2 : une dimension ne peut pas être négative.
Fin de la tentative de calcul.

Le point clé. e.dimension et e.valeur sont accessibles sans aucun as : le test instanceof a réduit le type de e d’unknown à ErreurDimensionNegative dans la branche correspondante. C’est le même mécanisme que le prédicat de type du chapitre 5.1, sauf qu’ici il est fourni gratuitement par le langage.

C’est aussi ce qui justifie d’écrire ses propres classes d’erreur. Avec un Error générique, la seule façon de savoir quelle dimension était fautive serait de fouiller le texte du message :

if (e.message.includes("Longueur")) { ... }   // à ne pas faire

C’est fragile (une faute de frappe, une traduction, une reformulation, et tout casse) et inutilisable pour récupérer la valeur numérique. Une classe d’erreur transporte des données structurées : c’est sa raison d’être.

Un piège technique à connaître : si votre tsconfig.json cible "target": "es5", tous les instanceof sur des sous-classes d’Error deviennent faux : la chaîne de prototypes est cassée par la transpilation. Le remède est Object.setPrototypeOf(this, ErreurDimensionNegative.prototype); juste après super(...). Avec une cible es2015 ou supérieure, qui est la norme aujourd’hui, le problème ne se pose pas.

} catch (e: unknown) {
  if (e instanceof Error) {
    console.error("Erreur inattendue :", e.message);
  } else if (e instanceof ErreurDimensionNegative) {
    console.error(`La ${e.dimension} vaut ${e.valeur}...`);   // jamais atteint
  } else if (e instanceof ErreurDimensionNulle) {
    console.error(`La ${e.dimension} est nulle...`);          // jamais atteint
  }
}

Nouvelle sortie :

Surface du rectangle 5 x 4 : 20
Fin de la tentative de calcul.
Erreur inattendue : largeur nulle
Fin de la tentative de calcul.
Erreur inattendue : longueur négative : -2
Fin de la tentative de calcul.

Les deux branches spécifiques ne sont jamais atteintes. La raison tient à l’héritage : ErreurDimensionNegative extends Error, donc e instanceof Error est vrai pour elle aussi. Le premier if capture tout.

Ce qui rend le piège redoutable, c’est que TypeScript ne dit rien. Java refuse de compiler un catch (Exception e) placé avant un catch (IOException e), en signalant que le second est inatteignable. Ici, pas d’avertissement : vous perdez silencieusement tous vos messages spécifiques, et la seule façon de vous en rendre compte est de lire la sortie. D’où la règle : du plus spécifique au plus général, Error toujours en dernier.

Exercice 5.10 : Exception ou valeur de retour ?

approfondissement

Pour chacune de ces quatre fonctions, décidez si elle doit lever une exception ou renvoyer une valeur, implémentez votre choix, et justifiez en une phrase de commentaire :

  1. racineCarree(x: number): number sur un nombre négatif ;
  2. chercherUtilisateur(id: string) quand l’identifiant n’existe pas dans le tableau ;
  3. parserAge(saisie: string) sur une saisie de formulaire non numérique ;
  4. ouvrirConfiguration(chemin: string) quand le fichier est absent au démarrage de l’application.

Critère de réussite : au moins une fonction utilise | undefined plutôt qu’une exception, et vous savez expliquer pourquoi.

Le critère de tri tient en une question : l’appelant peut-il raisonnablement s’attendre à ce cas et le traiter sur place ? Si oui, c’est un résultat, donc une valeur de retour. Si non, c’est une anomalie, donc une exception.

Fonction Choix Raison
racineCarree exception un argument négatif est un bug de l’appelant
chercherUtilisateur | undefined ne rien trouver est un résultat normal
parserAge valeur de retour une saisie fausse est le cas nominal d’un formulaire
ouvrirConfiguration exception l’application ne peut pas démarrer sans
// 1. EXCEPTION. Un argument négatif est une erreur de programmation de l'appelant :
//    il n'existe aucune valeur correcte à renvoyer, et NaN se propagerait en silence.
function racineCarree(x: number): number {
  if (x < 0) {
    throw new Error(`racineCarree n'accepte pas d'argument négatif (reçu : ${x})`);
  }
  return Math.sqrt(x);
}

// 2. VALEUR DE RETOUR. « Ne pas trouver » est un résultat normal d'une recherche.
//    Le | undefined force l'appelant à traiter le cas, ce qu'une exception ne fait pas.
interface Utilisateur {
  id: string;
  nom: string;
}

const utilisateurs: Utilisateur[] = [
  { id: "u1", nom: "Alice" },
  { id: "u2", nom: "Bob" },
];

function chercherUtilisateur(id: string): Utilisateur | undefined {
  return utilisateurs.find((u) => u.id === id);
}

const trouve = chercherUtilisateur("u3");
// console.log(trouve.nom);
// ERREUR : 'trouve' is possibly 'undefined'.  ← le compilateur vous oblige à décider
console.log(trouve?.nom ?? "utilisateur inconnu");

// 3. VALEUR DE RETOUR. Une saisie invalide est le cas NOMINAL d'un formulaire :
//    il faut réafficher le champ avec un message, pas interrompre le programme.
type ResultatAge =
  | { ok: true; age: number }
  | { ok: false; message: string };

function parserAge(saisie: string): ResultatAge {
  const texte = saisie.trim();
  const n = Number(texte);

  if (texte === "" || Number.isNaN(n)) {
    return { ok: false, message: "Entrez un nombre." };
  }
  if (!Number.isInteger(n) || n < 0 || n > 130) {
    return { ok: false, message: "Entrez un âge entier entre 0 et 130." };
  }
  return { ok: true, age: n };
}

const resultat = parserAge("abc");
if (resultat.ok) {
  console.log(`Âge : ${resultat.age}`);
} else {
  console.error(resultat.message);   // Entrez un nombre.
}

// 4. EXCEPTION. Sans configuration, l'application ne PEUT PAS démarrer :
//    continuer dans un état incohérent est pire que s'arrêter avec un message clair.
declare function lireFichier(chemin: string): string | null;

class ErreurConfigurationAbsente extends Error {
  constructor(public chemin: string) {
    super(`Configuration introuvable : ${chemin}`);
    this.name = "ErreurConfigurationAbsente";
  }
}

interface Configuration {
  port: number;
}

function ouvrirConfiguration(chemin: string): Configuration {
  const contenu = lireFichier(chemin);
  if (contenu === null) {
    throw new ErreurConfigurationAbsente(chemin);
  }
  return JSON.parse(contenu);
}

Le point clé. Comparez les cas 2 et 4, qui se ressemblent pourtant beaucoup : dans les deux, « quelque chose n’est pas là ». La différence n’est pas technique, elle est sémantique : un utilisateur absent est un résultat que l’appelant saura gérer (afficher « inconnu »), tandis qu’un fichier de configuration absent au démarrage ne laisse aucune suite possible. La question n’est jamais « est-ce grave ? » mais « l’appelant a-t-il quelque chose à faire de cette information ? ».

L’argument décisif en faveur de la valeur de retour est la vérification par le compilateur. Utilisateur | undefined apparaît dans la signature, et TypeScript refuse de lire .nom sans avoir traité le cas. Une exception, elle, ne figure nulle part dans le type : rien ne vous rappellera qu’il faut l’attraper, et l’oubli ne se verra qu’en production.

Variante pour le cas 3 : renvoyer number | undefined, plus léger à écrire, mais vous perdez le message à afficher à l’utilisateur et la distinction entre « ce n’est pas un nombre » et « ce n’est pas un âge plausible ». L’union discriminée { ok: true ... } | { ok: false ... } est le schéma standard : le champ ok sert de discriminant, et c’est lui qui permet à TypeScript de savoir que resultat.age n’existe que dans la branche ok === true.

L’erreur classique de l’exercice est le cas 1 inversé : faire renvoyer NaN à racineCarree, ce que fait d’ailleurs Math.sqrt(-1). NaN contamine tout calcul qui le touche, sans jamais lever d’erreur ; vous retrouvez la trace du problème dix fonctions plus loin, dans un affichage incompréhensible.

QCM

1. Qu’affiche ce code ?

try {
  console.log("A");
  throw new Error("oups");
  console.log("B");
} catch (e) {
  console.log("C");
} finally {
  console.log("D");
}

A. A B C D B. A C D C. A C D. A D

B. throw interrompt le try : B n’est jamais atteint. Le catch s’exécute, puis le finally, qui s’exécute dans tous les cas.

2. En mode strict, que répond le compilateur ?

try {
  risque();
} catch (e) {
  console.log(e.message);
}

A. Rien : e est une Error. B. Une erreur : e est de type unknown, il faut d’abord tester e instanceof Error. C. Rien : e est de type any. D. Une erreur : un catch doit toujours annoter e: Error.

B. N’importe quelle valeur peut être levée, donc TypeScript ne suppose rien. L’annotation e: Error est d’ailleurs refusée : seuls unknown et any sont permis.

3. Qu’affiche ce code ?

class ErreurStock extends Error {}

try {
  throw new ErreurStock("rupture");
} catch (e: unknown) {
  if (e instanceof Error) {
    console.log("erreur générale");
  } else if (e instanceof ErreurStock) {
    console.log("rupture de stock");
  }
}

A. rupture de stock B. erreur générale C. Les deux messages. D. Rien.

B. Une ErreurStock est aussi une Error : le premier test réussit et la branche suivante n’est jamais essayée. Testez du plus spécifique au plus général.

À retenir

  • throw interrompt tout et remonte la pile jusqu’au premier try rencontré
  • Levez toujours un Error ou une sous-classe : jamais une chaîne, jamais un nombre
  • finally s’exécute dans tous les cas ; n’y mettez jamais de return
  • En TypeScript, e est unknown : testez if (e instanceof Error) avant de l’utiliser
  • Classes d’erreur personnalisées : extends Error + super(message) + données utiles
  • Testez les instanceof du plus spécifique au plus général
  • L’absence de résultat n’est pas une erreur : renvoyez T | undefined