5.3 · Exceptions
Les diapositives de la séance 5
- lever une exception avec
throwet la rattraper avectry/catch/finally - expliquer pourquoi la variable de
catchest de typeunknownet la traiter proprement - créer ses propres classes d’erreur héritant de
Error - distinguer plusieurs erreurs avec
instanceofet 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.
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ébogageCréer ses propres classes d’erreur
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 desError, elles en héritentmessage,nameetstack;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 bloccatchpourra 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
catchvide (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));- avant d’exécuter, écrivez sur papier la sortie attendue pour chaque appel ;
- exécutez et comparez ;
- remplacez le contenu du
finallyparreturn "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.
return dans le finally
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 bienIl 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 :
- créez une classe
Rectangleaveclongueuretlargeur, initialisées par le constructeur ; - ajoutez
calculerSurface(): numberqui lève uneErrorsilongueuroulargeurest négative ou nulle, et le message doit indiquer quelle dimension pose problème ; - ajoutez
essayerCalculSurface(): voidqui appellecalculerSurface()dans untry/catch/finally, affiche la surface ou le message d’erreur, et signale la fin du traitement dans lefinally; - 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 :
ErreurDimensionNegative extends Error, portant une propriétédimension: "longueur" | "largeur"et lavaleur: numberfautive ;ErreurDimensionNulle extends Error, portant la même propriétédimension;- adaptez
calculerSurface()pour lever la bonne erreur selon le cas ; - dans le
catch, distinguez les trois cas (ErreurDimensionNegative,ErreurDimensionNulle, puisError) et affichez un message différent utilisant les propriétés des erreurs, jamais leur message texte ; - inversez volontairement l’ordre des tests en plaçant
e instanceof Erroren 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 faireC’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 :
racineCarree(x: number): numbersur un nombre négatif ;chercherUtilisateur(id: string)quand l’identifiant n’existe pas dans le tableau ;parserAge(saisie: string)sur une saisie de formulaire non numérique ;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
throwinterrompt tout et remonte la pile jusqu’au premiertryrencontré- Levez toujours un
Errorou une sous-classe : jamais une chaîne, jamais un nombre finallys’exécute dans tous les cas ; n’y mettez jamais dereturn- En TypeScript,
eestunknown: testezif (e instanceof Error)avant de l’utiliser - Classes d’erreur personnalisées :
extends Error+super(message)+ données utiles - Testez les
instanceofdu plus spécifique au plus général - L’absence de résultat n’est pas une erreur : renvoyez
T | undefined