Quan desenvolupem alguna cosa, sovint necessitem les nostres pròpies classes d’error per reflectir coses específiques que poden sortir malament a les nostres tasques. Per errors en les operacions de xarxa, podem necessitar HttpError
, per a les operacions de la base de dades DbError
, per a les operacions de recerca NotFoundError
, etc.
Els nostres errors han d’admetre propietats d’error bàsiques com message
, name
i, preferiblement, stack
. Però també poden tenir altres propietats pròpies, per exemple, els objectes HttpError
poden tenir una propietat statusCode
amb un valor com a 404
o 403
o 500
.
JavaScript permet utilitzar throw
amb qualsevol argument, de manera que tècnicament les nostres classes d’error personalitzades no necessiten heretar de Error
. Però si vam heretar, llavors és possible usar obj instanceof Error
per identificar objectes error. Llavors és millor heretar d’ell.
A mesura que l’aplicació creix, els nostres propis errors formen naturalment una jerarquia. Per exemple, HttpTimeoutError
pot heretar de HttpError
, i així successivament.
Estenent Error
Com a exemple, considerem una funció readUser(json)
que hauria de llegir JSON amb les dades de l’usuari.
Aquí hi ha un exemple de com s’hi pot veure un json
vàlid:
let json = `{ "name": "John", "age": 30 }`;
Internament , farem servir JSON.parse
. Si rep json
mal format, llavors llança SyntaxError
. Però fins i tot si json
és sintàcticament correcte, això no vol dir que sigui un usuari vàlid, oi? Pot perdre les dades necessàries. Per exemple, pot no tenir propietats de nom i edat que són essencials per als nostres usuaris.
La nostra funció readUser(json)
no només llegirà JSON, sinó que verificarà ( “validarà “) les dades. Si no hi ha camps obligatoris, o el format és incorrecte, llavors és un error. I això no és un” SyntaxError “, perquè les dades són sintàcticament correctes, sinó un altre tipus d’error. L’anomenarem ValidationError
i crearem una classe per a això. Un error d’aquest tipus també ha de portar la informació sobre el camp infractor.
La nostra classe ValidationError
hauria heretar de la classe incorporada Error
.
Aquesta classe està incorporada, però aquí està la seva codi aproximat perquè puguem entendre el que estem estenent:
// El "pseudocódigo" para la clase Error incorporada definida por el propio JavaScriptclass Error { constructor(message) { this.message = message; this.name = "Error"; // (diferentes nombres para diferentes clases error incorporadas) this.stack = <call stack>; // no estándar, pero la mayoría de los entornos lo admiten }}
Ara heredemos ValidationError
i provem-ho en acció:
class ValidationError extends Error { constructor(message) { super(message); // (1) this.name = "ValidationError"; // (2) }}function test() { throw new ValidationError("Vaya!");}try { test();} catch(err) { alert(err.message); // Vaya! alert(err.name); // ValidationError alert(err.stack); // una lista de llamadas anidadas con números de línea para cada una}
Tingueu en compte: en la línia (1)
cridem a constructor pare. JavaScript requereix que truquem super
al constructor fill, de manera que és obligatori. El constructor pare estableix la propietat message
.
El constructor principal també estableix la propietat name
en "Error"
, de manera que en la línia (2)
la restablim a el valor correcte.
Intentem usar-lo en readUser(json)
:
class ValidationError extends Error { constructor(message) { super(message); this.name = "ValidationError"; }}// Usofunction readUser(json) { let user = JSON.parse(json); if (!user.age) { throw new ValidationError("Sin campo: age"); } if (!user.name) { throw new ValidationError("Sin campo: name"); } return user;}// Ejemplo de trabajo con try..catchtry { let user = readUser('{ "age": 25 }');} catch (err) { if (err instanceof ValidationError) { alert("Dato inválido: " + err.message); // Dato inválido: sin campo: nombre } else if (err instanceof SyntaxError) { // (*) alert("Error de sintaxis JSON: " + err.message); } else { throw err; // error desconocido, vuelva a lanzarlo (**) }}
el bloc try..catch
en el codi anterior fa servir tant el nostre ValidationError
com el SyntaxError
incorporat de JSON.parse
.
Observi com fem servir instanceof
per verificar el tipus d’error específic en la línia (*)
.
També podríem mirar err.name
, així:
// ...// en lugar de (err instanceof SyntaxError)} else if (err.name == "SyntaxError") { // (*)// ...
La versió instanceof
és molt millor, perquè en el futur anem a estendre , farem subtipus d’ella, com PropertyRequiredError
. I el control instanceof
continuarà funcionant per a les noves classes heretades. Llavors això és a prova de futur.
També és important que si catch
troba un error desconegut, llavors el torna a llançar a la línia (**)
. El bloc catch
només sap com manejar els errors de validació i sintaxi, altres tipus (a causa d’un error tipogràfic en el codi o altres desconeguts) han de fallar.
herència addicional
La classe ValidationError
és molt genèrica. Moltes coses poden sortir malament.La propietat pot estar absent o pot estar en un format incorrecte (com un valor de cadena per age
). Fem una classe més concreta PropertyRequiredError
, exactament per propietats absents. Portarà informació addicional sobre la propietat que falta.
class ValidationError extends Error { constructor(message) { super(message); this.name = "ValidationError"; }}class PropertyRequiredError extends ValidationError { constructor(property) { super("Sin propiedad: " + property); this.name = "PropertyRequiredError"; this.property = property; }}// Usofunction readUser(json) { let user = JSON.parse(json); if (!user.age) { throw new PropertyRequiredError("age"); } if (!user.name) { throw new PropertyRequiredError("name"); } return user;}// Ejemplo de trabajo con try..catchtry { let user = readUser('{ "age": 25 }');} catch (err) { if (err instanceof ValidationError) { alert("Dato inválido: " + err.message); // Dato inválido: Sin propiedad: name alert(err.name); // PropertyRequiredError alert(err.property); // name } else if (err instanceof SyntaxError) { alert("Error de sintaxis JSON: " + err.message); } else { throw err; // error desconocido, vuelva a lanzarlo }}
la nova classe PropertyRequiredError
és fàcil d’usar: només necessitem passar el nom de la propietat: new PropertyRequiredError(property)
. El message
llegible per humans és generat pel constructor.
Recordeu que this.name
al constructor PropertyRequiredError
s’assigna de nou manualment. Això pot tornar-se una mica tediós: assignar this.name = <class name>
en cada classe d’error personalitzada. Podem evitar-ho fent la nostra pròpia classe “error bàsic” que assigna this.name = this.constructor.name
. I després hereti tots els nostres errors personalitzats.
anomenem-MyError
.
Aquí està el codi amb MyError
i altres classes error personalitzades, simplificades:
class MyError extends Error { constructor(message) { super(message); this.name = this.constructor.name; }}class ValidationError extends MyError { }class PropertyRequiredError extends ValidationError { constructor(property) { super("sin propiedad: " + property); this.property = property; }}// name es incorrectoalert( new PropertyRequiredError("campo").name ); // PropertyRequiredError
Ara els errors personalitzats són molt més curts, especialment ValidationError
, ja que eliminem la línia "this.name = ..."
al constructor.
Empacat de Excepcions
el propòsit de la funció readUser
en el codi anterior és “llegir les dades de l’usuari”. Hi pot haver diferents tipus d’errors en el procés. En aquest moment tenim SyntaxError
i ValidationError
, però en el futur la funció readUser
pot créixer i probablement generar altres tipus d’errors.
El codi que crida a readUser
ha de gestionar aquests errors. En aquest moment utilitza múltiples if
en el bloc catch
, que verifiquen la classe i manegen els errors coneguts i tornen a llançar els desconeguts.
L’esquema és així:
try { ... readUser() // la fuente potencial de error ...} catch (err) { if (err instanceof ValidationError) { // manejar errores de validación } else if (err instanceof SyntaxError) { // manejar errores de sintaxis } else { throw err; // error desconocido, vuelva a lanzarlo }}
En el codi anterior podem veure dos tipus d’errors, però pot haver-hi més.
Si la funció readUser
genera diversos tipus d’errors, llavors hem de preguntar: ¿ realment volem verificar tots els tipus d’error un per un cada vegada?
sovint, la resposta és “No”: ens agradaria estar “un nivell per sobre de tot això”. Només volem saber si hi va haver un “error de lectura de dades”: el per què va passar exactament és sovint irrellevant (el missatge d’error el descriu). O, millor encara, ens agradaria tenir una forma d’obtenir els detalls de l’error, però només si cal.
La tècnica que descrivim aquí es diu “embalatge d’excepcions”.
- Crearem una nova classe
ReadError
per representar un error genèric de “lectura de dades”. - La funció
readUser
detectarà els errors de lectura de dades que ocorren dins d’ella, comValidationError
iSyntaxError
, i generarà unReadError
al seu lloc. - l’objecte
ReadError
mantindrà la referència a l’error original en la seva propietatcause
.
Llavors, el codi que crida a readUser
només haurà de verificar ReadError
, no tots els tipus d’errors de lectura de d ats. I si necessita més detalls d’un error, pot verificar la seva propietat cause
.
Aquí està el codi que defineix ReadError
i demostra el seu ús en readUser
i 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("Error de sintaxis", err); } else { throw err; } } try { validateUser(user); } catch (err) { if (err instanceof ValidationError) { throw new ReadError("Error de validación", err); } else { throw err; } }}try { readUser('{json malo}');} catch (e) { if (e instanceof ReadError) { alert(e); // Error original: SyntaxError: inesperado token b en JSON en la posición 1 alert("Error original: " + e.cause); } else { throw e; }}
En el codi anterior, readUser
funciona exactament com es descriu: detecta els errors de sintaxi i validació i llança els errors ReadError
en el seu lloc (els errors desconeguts es tornen a generar com de costum).
Llavors, el codi extern verifica instanceof ReadError
i això és tot. No cal enumerar tots els tipus d’error possibles.
L’enfocament es diu “embalatge d’excepcions”, perquè prenem excepcions de “baix nivell” i les “ajustem” en ReadError
que és més abstracte. és àmpliament utilitzat en la programació orientada a objectes.
Resum
- Podem heretar de
Error
i altres classes d’error incorporades normalment. Només ens cal tenir cura de la propietatname
i no oblidem cridarsuper
. - Podem fer servir
instanceof
per verificar errors particulars. També funciona amb herència.Però de vegades tenim un objecte error que prové d’una biblioteca de tercers i no hi ha una manera fàcil d’ obtenir la seva classe. Llavors la propietatname
pot usar-se per tals controls. - Empacat d’excepcions és una tècnica generalitzada : una funció maneja excepcions de baix nivell i crea errors d’alt nivell en lloc de diversos errors de baix nivell. Les excepcions de baix nivell de vegades es converteixen en propietats d’aquest objecte com
err.cause
en els exemples anteriors , però això no és estrictament necessari.