Errors personalitzats, estenent Error

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”.

  1. Crearem una nova classe ReadError per representar un error genèric de “lectura de dades”.
  2. La funció readUser detectarà els errors de lectura de dades que ocorren dins d’ella, com ValidationError i SyntaxError, i generarà un ReadError al seu lloc.
  3. l’objecte ReadError mantindrà la referència a l’error original en la seva propietat cause.

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 propietat name i no oblidem cridar super.
  • 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 propietat name 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.

Deixa un comentari

L'adreça electrònica no es publicarà. Els camps necessaris estan marcats amb *