Lorsque nous développons quelque chose, nous avons souvent besoin de nos propres classes dâerreur pour refléter des problèmes spécifiques qui peuvent mal tourner dans nos tâches. Pour les erreurs dans les opérations réseau, nous aurons peut-être besoin de HttpError, pour les opérations de base de données DbError, pour les opérations de recherche NotFoundError, etc.
Nos erreurs devraient prendre en charge des propriétés dâerreur de base telles que message, name et, de préférence, stack. Mais elles peuvent aussi avoir dâautres propriétés propres, par exemple les objets HttpError peuvent avoir une propriété statusCode avec une valeur telle que 404 ou 403 ou 500.
JavaScript permet dâutiliser throw avec nâimporte quel argument. Par conséquent, techniquement, nos classes dâerreur personnalisées nâont pas besoin dâhériter de Error. Mais si nous héritons, il devient alors possible dâutiliser obj instanceof Error pour identifier les objets dâerreur. Il vaut donc mieux en hériter.
Au fur et à mesure que lâapplication grandit, nos propres erreurs forment naturellement une hiérarchie. Par exemple, HttpTimeoutError peut hériter de HttpError, etc.
Ãtendre Error
A titre dâexemple, considérons une fonction readUser(json) qui devrait lire JSON avec des données utilisateur.
Voici un exemple de lâapparence dâun json valide :
let json = `{ "name": "John", "age": 30 }`;
En interne, nous utiliserons JSON.parse. Sâil reçoit un json malformé, il renvoie SyntaxError. Mais même si json est syntaxiquement correct, cela ne signifie pas que câest un utilisateur valide, non ? Il peut manquer les données nécessaires. Par exemple, il peut ne pas avoir les propriétés name et age qui sont essentielles pour nos utilisateurs.
Notre fonction readUser(json) va non seulement lire JSON, mais aussi vérifier (âvaliderâ) les données. Sâil nây a pas de champs obligatoires ou si le format est incorrect, câest une erreur. Et ce nâest pas une SyntaxError, car les données sont syntaxiquement correctes, mais un autre type dâerreur. Nous lâappellerons ValidationError et créerons une classe pour cela. Une erreur de ce type devrait également comporter des informations sur le champ fautif.
Notre classe ValidationError devrait hériter de la classe Error.
La class Error est une classe intégrée, voici le code approximatif pour que nous comprenions ce que nous étendons :
// Le "pseudocode" pour la classe d'erreur intégrée définie par JavaScript lui-même
class Error {
constructor(message) {
this.message = message;
this.name = "Error"; // (noms différents pour différentes classes d'erreur intégrées)
this.stack = <call stack>; // non standard, mais la plupart des environnements le supportent
}
}
Maintenant, héritons de ValidationError et mettons-le en action :
class ValidationError extends Error {
constructor(message) {
super(message); // (1)
this.name = "ValidationError"; // (2)
}
}
function test() {
throw new ValidationError("Whoops!");
}
try {
test();
} catch(err) {
alert(err.message); // Whoops!
alert(err.name); // ValidationError
alert(err.stack); // une liste des appels imbriqués avec le numéro de ligne pour chacun d'entre eux
}
Remarque : à la ligne (1), nous appelons le constructeur parent. JavaScript exige que nous appelions super dans le constructeur de lâenfant, donc câest obligatoire. Le constructeur parent définit la propriété message.
Le constructeur parent définit également la propriété name sur "Error", donc à la ligne (2) nous la réinitialisons à la valeur correcte.
Essayons de lâutiliser dans readUser(json) :
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
// Usage
function readUser(json) {
let user = JSON.parse(json);
if (!user.age) {
throw new ValidationError("No field: age");
}
if (!user.name) {
throw new ValidationError("No field: name");
}
return user;
}
// un example avec try..catch
try {
let user = readUser('{ "age": 25 }');
} catch (err) {
if (err instanceof ValidationError) {
alert("Invalid data: " + err.message); // Invalid data: No field: name
} else if (err instanceof SyntaxError) { // (*)
alert("JSON Syntax Error: " + err.message);
} else {
throw err; // erreur inconnue, on la propage (**)
}
}
Le bloc try..catch dans le code ci-dessus gère à la fois notre ValidationError et le SyntaxError intégré de JSON.parse.
Veuillez regarder comment nous utilisons instanceof pour vérifier le type dâerreur spécifique à la ligne (*).
Nous pourrions aussi regarder err.name, comme ceci :
// ...
// au lieu de (err instanceof SyntaxError)
} else if (err.name == "SyntaxError") { // (*)
// ...
La version instanceof est bien meilleure, car dans le futur nous allons étendre ValidationError, en créer des sous-types, comme PropertyRequiredError. Et instanceof continuera à fonctionner pour les nouvelles classes héritées. Donc, câest à lâépreuve du futur.
Il est également important que si catch rencontre une erreur inconnue, il la renvoie à la ligne (**). Le bloc catch ne sait gérer que les erreurs de validation et de syntaxe, les autres types (causés par une faute de frappe dans le code ou dâautres raisons inconnues) devraient êtres propagés.
Héritage complémentaire
La classe ValidationError est très générique. Beaucoup de choses peuvent mal se passer. La propriété peut être absente ou dans un format incorrect (comme une valeur de chaîne de caractères pour age au lieu dâun nombre). Faisons une classe plus concrète PropertyRequiredError, exactement pour les propriétés absentes. Elle contiendra des informations supplémentaires sur la propriété qui manque.
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
class PropertyRequiredError extends ValidationError {
constructor(property) {
super("No property: " + property);
this.name = "PropertyRequiredError";
this.property = property;
}
}
// Usage
function readUser(json) {
let user = JSON.parse(json);
if (!user.age) {
throw new PropertyRequiredError("age");
}
if (!user.name) {
throw new PropertyRequiredError("name");
}
return user;
}
// example avec try..catch
try {
let user = readUser('{ "age": 25 }');
} catch (err) {
if (err instanceof ValidationError) {
alert("Invalid data: " + err.message); // Invalid data: No property: name
alert(err.name); // PropertyRequiredError
alert(err.property); // name
} else if (err instanceof SyntaxError) {
alert("JSON Syntax Error: " + err.message);
} else {
throw err; // erreur inconnue, on la propage
}
}
La nouvelle classe PropertyRequiredError est facile à utiliser : il suffit de passer le nom de la propriété : new PropertyRequiredError(property). Le message est généré par le constructeur.
Veuillez noter que this.name dans le constructeur PropertyRequiredError est à nouveau attribué manuellement. Cela peut devenir un peu fastidieux â dâassigner this.name = <class name> dans chaque classe dâerreur personnalisée. Nous pouvons lâéviter en créant notre propre classe âdâerreur de baseâ qui assigne this.name = this.constructor.name. Puis nous en ferons hériter toutes nos classes dâerreur personnalisées.
Appelons cela MyError.
Voici le code avec MyError et dâautres classes dâerreur personnalisées, simplifié :
class MyError extends Error {
constructor(message) {
super(message);
this.name = this.constructor.name;
}
}
class ValidationError extends MyError { }
class PropertyRequiredError extends ValidationError {
constructor(property) {
super("No property: " + property);
this.property = property;
}
}
// le nom est correcte
alert( new PropertyRequiredError("field").name ); // PropertyRequiredError
Maintenant, les erreurs personnalisées sont beaucoup plus courtes, en particulier ValidationError, car nous nous sommes débarrassés de la ligne "this.name = ..." dans le constructeur.
Le wrapping des exceptions
Le but de la fonction readUser dans le code ci-dessus est âde lire les données de lâutilisateurâ. Il peut y avoir différents types dâerreurs dans le processus. à lâheure actuelle, nous avons SyntaxError et ValidationError, mais à lâavenir, la fonction readUser pourrait croître et générer probablement dâautres types dâerreurs.
Le code qui appelle readUser devrait gérer ces erreurs. à lâheure actuelle, il utilise plusieurs if dans le bloc catch, qui vérifient la classe et gèrent les erreurs connues et rejettent les inconnues.
Le schéma est le suivant :
try {
...
readUser() // la source d'erreur potentielle
...
} catch (err) {
if (err instanceof ValidationError) {
// handle validation errors
} else if (err instanceof SyntaxError) {
// handle syntax errors
} else {
throw err; // erreur inconnue, la relancer
}
}
Dans le code ci-dessus, nous pouvons voir deux types dâerreurs, mais il peut y en avoir plus.
Si la fonction readUser génère plusieurs types dâerreurs, alors nous devrions nous demander : voulons-nous vraiment vérifier tous les types dâerreur un par un à chaque fois ?
Souvent, la réponse est ânonâ, nous aimerions être âun niveau au-dessus de tout celaâ. Nous voulons simplement savoir sâil y a eu une âerreur de lecture des donnéesâ â pourquoi exactement cela sâest produit est souvent hors de propos (le message dâerreur le décrit). Ou, encore mieux, nous aimerions avoir un moyen dâobtenir les détails de lâerreur, mais seulement si nous en avons besoin.
La technique que nous décrivons ici est appelée âencapsulation dâexceptionsâ.
- Nous allons créer une nouvelle classe
ReadErrorpour représenter une erreur générique de âlecture des donnéesâ. - La fonction
readUserinterceptera les erreurs de lecture de données qui se produisent à lâintérieur, telles queValidationErroretSyntaxError, et générera à la place uneReadError. - Lâobjet
ReadErrorconservera la référence à lâerreur dâorigine dans sa propriétécause.
Ensuite, le code qui appelle readUser nâaura quâà vérifier ReadError, pas pour tous les types dâerreurs de lecture de données. Et sâil a besoin de plus de détails sur une erreur, il peut vérifier sa propriété cause.
Voici le code qui définit ReadError et illustre son utilisation dans readUser et try..catch :
class ReadError extends Error {
constructor(message, cause) {
super(message);
this.cause = cause;
this.name = 'ReadError';
}
}
class ValidationError extends Error { /*...*/ }
class PropertyRequiredError extends ValidationError { /* ... */ }
function validateUser(user) {
if (!user.age) {
throw new PropertyRequiredError("age");
}
if (!user.name) {
throw new PropertyRequiredError("name");
}
}
function readUser(json) {
let user;
try {
user = JSON.parse(json);
} catch (err) {
if (err instanceof SyntaxError) {
throw new ReadError("Syntax Error", err);
} else {
throw err;
}
}
try {
validateUser(user);
} catch (err) {
if (err instanceof ValidationError) {
throw new ReadError("Validation Error", err);
} else {
throw err;
}
}
}
try {
readUser('{bad json}');
} catch (e) {
if (e instanceof ReadError) {
alert(e);
// Original error: SyntaxError: Unexpected token b in JSON at position 1
alert("Original error: " + e.cause);
} else {
throw e;
}
}
Dans le code ci-dessus, readUser fonctionne exactement comme décrit â il intercepte les erreurs de syntaxe et de validation et propage des erreurs ReadError (les erreurs inconnues sont propagées comme dâhabitude).
Donc, le code externe vérifie instanceof ReadError et câest tout. Pas besoin de lister tous les types dâerreur possibles.
Lâapproche est appelée âencapsulation dâexceptionsâ, car nous prenons les exceptions âde bas niveauâ et les âencapsulonsâ dans ReadError qui est plus abstrait. Il est largement utilisé dans la programmation orientée objet.
Résumé
- Nous pouvons hériter de
Erroret dâautres classes dâerreurs intégrées normalement. Nous devons juste nous occuper de la propriéténameet ne pas oublier dâappelersuper. - Nous pouvons utiliser
instanceofpour vérifier des erreurs particulières. Cela fonctionne aussi avec lâhéritage. Mais parfois, nous avons un objet dâerreur provenant dâune bibliothèque tierce et il nây a pas de moyen facile dâobtenir la classe. Dans ce cas, la propriéténamepeut être utilisée pour de telles vérifications. - Le wrapping des exceptions est une technique répandue : une fonction gère les exceptions de bas niveau et crée des erreurs de niveau supérieur au lieu de diverses erreurs de bas niveau. Les exceptions de bas niveau deviennent parfois des propriétés de cet objet comme
err.causedans les exemples ci-dessus, mais ce nâest pas strictement requis.
Commentaires
<code>, pour plusieurs lignes â enveloppez-les avec la balise<pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepenâ¦)