Lorsque nous développons quelque chose, nous avons souvent besoin de nos propres classes d'erreurs pour refléter des éléments spécifiques qui peuvent mal tourner dans nos tâches. Pour les erreurs dans les opérations réseau, nous pouvons avoir besoin HttpError
, pour les opérations de base de données DbError
, pour les opérations de recherche NotFoundError
et ainsi de suite.
Nos erreurs doivent prendre en charge les propriétés d'erreur de base telles que message
, name
et, de préférence, stack
. Mais ils peuvent également avoir d'autres propriétés qui leur sont 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, donc 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.
À mesure que l’application se développe, nos propres erreurs forment naturellement une hiérarchie. Par exemple, HttpTimeoutError
peut hériter de HttpError
, et ainsi de suite.
À titre d'exemple, considérons une fonction readUser(json)
qui doit lire JSON avec les données utilisateur.
Voici un exemple de ce à quoi peut ressembler un json
valide :
let json = `{ "name": "John", "age": 30 }`;
En interne, nous utiliserons JSON.parse
. S'il reçoit json
mal formé, il renvoie SyntaxError
. Mais même si json
est syntaxiquement correct, cela ne signifie pas que c'est un utilisateur valide, n'est-ce pas ? Il se peut qu'il manque les données nécessaires. Par exemple, il se peut qu’il ne comporte pas de propriétés name
et age
essentielles pour nos utilisateurs.
Notre fonction readUser(json)
va non seulement lire JSON, mais vérifier (« valider ») les données. S'il n'y a aucun champ obligatoire 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 doit également contenir des informations sur le champ incriminé.
Notre classe ValidationError
doit hériter de la classe Error
.
La classe Error
est intégrée, mais voici son code approximatif afin que nous puissions comprendre ce que nous étendons :
// Le "pseudocode" de la classe Error intégrée définie par JavaScript lui-même Erreur de classe { constructeur (message) { ce.message = message ; this.name = "Erreur" ; // (différents noms pour différentes classes d'erreurs intégrées) this.stack = <pile d'appels>; // non standard, mais la plupart des environnements le prennent en charge } }
Héritons maintenant de ValidationError
et essayons-le en action :
la classe ValidationError étend l'erreur { constructeur (message) { super(message); // (1) this.name = "ValidationError"; // (2) } } fonction test() { throw new ValidationError("Whoops!"); } essayer { test(); } attraper (erreur) { alert(err.message); // Oups ! alert(err.name); // Erreur de validation alert(err.stack); // une liste d'appels imbriqués avec des numéros de ligne pour chacun }
Attention : dans la ligne (1)
nous appelons le constructeur parent. JavaScript nous oblige à appeler super
dans le constructeur enfant, c'est donc obligatoire. Le constructeur parent définit la propriété message
.
Le constructeur parent définit également la propriété name
sur "Error"
, donc dans la ligne (2)
nous la réinitialisons à la bonne valeur.
Essayons de l'utiliser dans readUser(json)
:
la classe ValidationError étend l'erreur { constructeur (message) { super(message); this.name = "ValidationError"; } } // Utilisation fonction readUser (json) { laissez l'utilisateur = JSON.parse(json); si (!user.age) { throw new ValidationError("Aucun champ : âge"); } si (!user.name) { throw new ValidationError("Aucun champ : nom"); } retourner l'utilisateur ; } // Exemple de travail avec try..catch essayer { let user = readUser('{ "age": 25 }'); } attraper (erreur) { if (erreur instanceof ValidationError) { alert("Données invalides : " + err.message); // Données invalides : Aucun champ : nom } else if (erreur instanceof SyntaxError) { // (*) alert("Erreur de syntaxe JSON : " + err.message); } autre { jeter l'erreur; // erreur inconnue, relancez-la (**) } }
Le bloc try..catch
dans le code ci-dessus gère à la fois notre ValidationError
et le SyntaxError
intégré de JSON.parse
.
Veuillez jeter un œil à la façon dont nous utilisons instanceof
pour vérifier le type d'erreur spécifique dans la ligne (*)
.
Nous pourrions également regarder err.name
, comme ceci :
//... // au lieu de (erreur instanceof SyntaxError) } else if (err.name == "SyntaxError") { // (*) //...
La version instanceof
est bien meilleure, car à l'avenir nous allons étendre ValidationError
, en créer des sous-types, comme PropertyRequiredError
. Et instanceof
check continuera à fonctionner pour les nouvelles classes héritières. C’est donc à l’épreuve du temps.
Il est également important que si catch
rencontre une erreur inconnue, il la renvoie dans la ligne (**)
. Le bloc catch
sait uniquement comment gérer 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 échouer.
La classe ValidationError
est très générique. Beaucoup de choses peuvent mal tourner. La propriété peut être absente ou avoir un format incorrect (comme une valeur de chaîne pour age
au lieu d'un nombre). Créons une classe plus concrète PropertyRequiredError
, exactement pour les propriétés absentes. Il contiendra des informations supplémentaires sur la propriété manquante.
la classe ValidationError étend l'erreur { constructeur (message) { super(message); this.name = "ValidationError"; } } la classe PropertyRequiredError étend ValidationError { constructeur (propriété) { super("Aucune propriété : " + propriété); this.name = "PropertyRequiredError" ; this.property = propriété ; } } // Utilisation fonction readUser (json) { laissez l'utilisateur = JSON.parse(json); si (!user.age) { lancer un nouveau PropertyRequiredError("age"); } si (!user.name) { lancer un nouveau PropertyRequiredError("nom"); } retourner l'utilisateur ; } // Exemple de travail avec try..catch essayer { let user = readUser('{ "age": 25 }'); } attraper (erreur) { if (erreur instanceof ValidationError) { alert("Données invalides : " + err.message); // Données invalides : Aucune propriété : nom alert(err.name); // PropriétéRequiredError alert(err.propriété); // nom } else if (erreur instanceof SyntaxError) { alert("Erreur de syntaxe JSON : " + err.message); } autre { jeter l'erreur; // erreur inconnue, relancez-la } }
La nouvelle classe PropertyRequiredError
est simple à utiliser : il suffit de transmettre le nom de la propriété : new PropertyRequiredError(property)
. Le message
lisible par l'homme 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 – attribuer this.name = <class name>
dans chaque classe d'erreur personnalisée. Nous pouvons l'éviter en créant notre propre classe « erreur de base » qui attribue this.name = this.constructor.name
. Et puis en hériter de toutes nos erreurs personnalisées.
Appelons- MyError
.
Voici le code avec MyError
et d'autres classes d'erreur personnalisées, simplifié :
la classe MyError étend l'erreur { constructeur (message) { super(message); this.name = this.constructor.name; } } la classe ValidationError étend MyError { } la classe PropertyRequiredError étend ValidationError { constructeur (propriété) { super("Aucune propriété : " + propriété); this.property = propriété ; } } // le nom est correct alert( new PropertyRequiredError("field").name ); // PropriétéRequiredError
Désormais, les erreurs personnalisées sont beaucoup plus courtes, en particulier ValidationError
, car nous avons supprimé la ligne "this.name = ..."
dans le constructeur.
Le but de la fonction readUser
dans le code ci-dessus est de « lire les données utilisateur ». Différents types d'erreurs peuvent survenir au cours du processus. À l'heure actuelle, nous avons SyntaxError
et ValidationError
, mais à l'avenir, la fonction readUser
pourrait se développer et probablement générer d'autres types d'erreurs.
Le code qui appelle readUser
doit gérer ces erreurs. À l'heure actuelle, il utilise plusieurs if
dans le bloc catch
, qui vérifient la classe, gèrent les erreurs connues et renvoient les erreurs inconnues.
Le schéma est le suivant :
essayer { ... readUser() // la source potentielle de l'erreur ... } attraper (erreur) { if (erreur instanceof ValidationError) { // gérer les erreurs de validation } else if (erreur instanceof SyntaxError) { // gère les erreurs de syntaxe } autre { jeter l'erreur; // erreur inconnue, relancez-la } }
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’erreurs un par un à chaque fois ?
Souvent la réponse est « Non » : nous aimerions être « un niveau au-dessus de tout ça ». Nous voulons simplement savoir s’il y a eu une « erreur de lecture des données » – la raison exacte qui s’est produite n’a souvent aucune importance (le message d’erreur le décrit). Ou, mieux encore, 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 « enveloppement d’exceptions ».
Nous allons créer une nouvelle classe ReadError
pour représenter une erreur générique de « lecture de données ».
La fonction readUser
détectera les erreurs de lecture de données qui se produisent à l'intérieur, telles que ValidationError
et SyntaxError
, et générera une ReadError
à la place.
L'objet ReadError
conservera 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 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 démontre son utilisation dans readUser
et try..catch
:
la classe ReadError étend l'erreur { constructeur (message, cause) { super(message); ceci.cause = cause ; this.name = 'ReadError'; } } la classe ValidationError étend l'erreur { /*...*/ } la classe PropertyRequiredError étend ValidationError { /* ... */ } fonction validateUser (utilisateur) { si (!user.age) { lancer un nouveau PropertyRequiredError("age"); } si (!user.name) { lancer un nouveau PropertyRequiredError("nom"); } } fonction readUser (json) { laisser l'utilisateur ; essayer { utilisateur = JSON.parse(json); } attraper (erreur) { if (erreur instanceof SyntaxError) { throw new ReadError("Erreur de syntaxe", err); } autre { jeter l'erreur; } } essayer { validateUser(utilisateur); } attraper (erreur) { if (erreur instanceof ValidationError) { throw new ReadError("Erreur de validation", err); } autre { jeter l'erreur; } } } essayer { readUser('{mauvais json}'); } attraper (e) { if (une instance de ReadError) { alerte(e); // Erreur d'origine : SyntaxError : jeton b inattendu dans JSON à la position 1 alert("Erreur d'origine : " + e.cause); } autre { lancez e; } }
Dans le code ci-dessus, readUser
fonctionne exactement comme décrit : détecte les erreurs de syntaxe et de validation et renvoie les erreurs ReadError
à la place (les erreurs inconnues sont renvoyées comme d'habitude).
Ainsi, le code externe vérifie instanceof ReadError
et c'est tout. Pas besoin de lister tous les types d’erreurs possibles.
L'approche est appelée « enveloppement des exceptions », car nous prenons les exceptions de « bas niveau » et les « enveloppons » dans ReadError
qui est plus abstrait. Il est largement utilisé en programmation orientée objet.
Nous pouvons normalement hériter d’ Error
et d’autres classes d’erreur intégrées. Nous devons juste nous occuper de la propriété name
et n'oubliez pas d'appeler super
.
Nous pouvons utiliser instanceof
pour vérifier des erreurs particulières. Cela fonctionne également avec l'héritage. Mais parfois, nous avons un objet d'erreur provenant d'une bibliothèque tierce et il n'existe pas de moyen simple d'obtenir sa classe. La propriété name
peut ensuite être utilisée pour de telles vérifications.
L'encapsulation 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.cause
dans les exemples ci-dessus, mais ce n'est pas strictement obligatoire.
importance : 5
Créez une classe FormatError
qui hérite de la classe SyntaxError
intégrée.
Il doit prendre en charge les propriétés message
, name
et stack
.
Exemple d'utilisation :
let err = new FormatError("erreur de formatage"); alert( err.message ); // erreur de formatage alert( err.name ); // Erreur de format alerte( err.stack ); // empiler alert( erreur instanceof FormatError ); // vrai alert( erreur instanceof SyntaxError ); // vrai (car hérite de SyntaxError)
la classe FormatError étend SyntaxError { constructeur (message) { super(message); this.name = this.constructor.name; } } let err = new FormatError("erreur de formatage"); alert( err.message ); // erreur de formatage alert( err.name ); // Erreur de format alerte( err.stack ); // empiler alert( erreur instanceof SyntaxError ); // vrai