Cuando desarrollamos algo, a menudo necesitamos nuestras propias clases de error para reflejar cosas específicas que pueden salir mal en nuestras tareas. Para errores en operaciones de red podemos necesitar HttpError
, para operaciones de base de datos DbError
, para operaciones de búsqueda NotFoundError
, etc.
Nuestros errores deben admitir propiedades de error básicas como message
, name
y, preferiblemente, stack
. Pero también pueden tener otras propiedades propias, por ejemplo, los objetos HttpError
pueden tener una propiedad statusCode
con un valor como 404
, 403
o 500
.
JavaScript permite usar throw
con cualquier argumento, por lo que técnicamente nuestras clases de error personalizadas no necesitan heredar de Error
. Pero si heredamos, entonces es posible utilizar obj instanceof Error
para identificar objetos de error. Por eso es mejor heredar de ello.
A medida que la aplicación crece, nuestros propios errores forman naturalmente una jerarquía. Por ejemplo, HttpTimeoutError
puede heredar de HttpError
, etc.
Como ejemplo, consideremos una función readUser(json)
que debería leer JSON con datos de usuario.
A continuación se muestra un ejemplo de cómo puede verse un json
válido:
let json = `{ "nombre": "Juan", "edad": 30 }`;
Internamente, usaremos JSON.parse
. Si recibe json
con formato incorrecto, arroja SyntaxError
. Pero incluso si json
es sintácticamente correcto, eso no significa que sea un usuario válido, ¿verdad? Es posible que se pierdan los datos necesarios. Por ejemplo, es posible que no tenga propiedades name
y age
que sean esenciales para nuestros usuarios.
Nuestra función readUser(json)
no solo leerá JSON, sino que también verificará (“validará”) los datos. Si no hay campos obligatorios o el formato es incorrecto, entonces se trata de un error. Y eso no es un SyntaxError
, porque los datos son sintácticamente correctos, sino otro tipo de error. Lo llamaremos ValidationError
y crearemos una clase para ello. Un error de ese tipo también debería contener información sobre el campo infractor.
Nuestra clase ValidationError
debería heredar de la clase Error
.
La clase Error
está integrada, pero aquí está su código aproximado para que podamos entender qué estamos ampliando:
// El "pseudocódigo" para la clase Error incorporada definida por el propio JavaScript Error de clase { constructor(mensaje) { este.mensaje = mensaje; this.nombre = "Error"; // (diferentes nombres para diferentes clases de errores integrados) this.stack = <pila de llamadas>; // no es estándar, pero la mayoría de los entornos lo admiten } }
Ahora heredemos ValidationError
y probémoslo en acción:
clase ValidationError extiende Error { constructor(mensaje) { super(mensaje); // (1) this.name = "ValidationError"; // (2) } } prueba de función() { lanzar un nuevo ValidationError ("¡Ups!"); } intentar { prueba(); } atrapar(errar) { alerta (mensaje de error); // ¡Vaya! alerta(error.nombre); // Error de validación alerta(err.pila); // una lista de llamadas anidadas con números de línea para cada una }
Tenga en cuenta: en la línea (1)
llamamos al constructor principal. JavaScript requiere que llamemos super
en el constructor secundario, por lo que es obligatorio. El constructor principal establece la propiedad message
.
El constructor principal también establece la propiedad name
en "Error"
, por lo que en la línea (2)
la restablecemos al valor correcto.
Intentemos usarlo en readUser(json)
:
clase ValidationError extiende Error { constructor(mensaje) { super(mensaje); this.name = "ValidationError"; } } // uso función leerUsuario(json) { dejar usuario = JSON.parse(json); si (!usuario.edad) { throw new ValidationError("Sin campo: edad"); } si (!nombre.usuario) { throw new ValidationError("Sin campo: nombre"); } usuario que regresa; } // Ejemplo de trabajo con try..catch intentar { let usuario = readUser('{ "edad": 25 }'); } atrapar (errar) { if (err instancia de ValidationError) { alert("Datos no válidos: " + err.message); // Datos no válidos: Sin campo: nombre } else if (err instancia de SyntaxError) { // (*) alert("Error de sintaxis JSON: " + err.message); } demás { tirar errar; // error desconocido, vuelve a lanzarlo (**) } }
El bloque try..catch
en el código anterior maneja tanto nuestro ValidationError
como el SyntaxError
integrado de JSON.parse
.
Observe cómo usamos instanceof
para verificar el tipo de error específico en la línea (*)
.
También podríamos mirar err.name
, así:
//... // en lugar de (err instancia de SyntaxError) } else if (err.name == "SyntaxError") { // (*) //...
La versión instanceof
es mucho mejor, porque en el futuro ampliaremos ValidationError
y crearemos subtipos del mismo, como PropertyRequiredError
. Y el control instanceof
seguirá funcionando para las nuevas clases heredadas. Así que eso está preparado para el futuro.
También es importante que si catch
encuentra un error desconocido, lo vuelva a lanzar en la línea (**)
. El bloque catch
solo sabe cómo manejar errores de validación y sintaxis; otros tipos (causados por un error tipográfico en el código u otras razones desconocidas) deberían fallar.
La clase ValidationError
es muy genérica. Muchas cosas pueden salir mal. La propiedad puede estar ausente o puede tener un formato incorrecto (como un valor de cadena para age
en lugar de un número). Hagamos una clase más concreta PropertyRequiredError
, exactamente para propiedades ausentes. Contendrá información adicional sobre la propiedad que falta.
clase ValidationError extiende Error { constructor(mensaje) { super(mensaje); this.name = "ValidationError"; } } clase PropertyRequiredError extiende ValidationError { constructor(propiedad) { super("Sin propiedad: " + propiedad); this.name = "PropertyRequiredError"; esta.propiedad = propiedad; } } // uso función leerUsuario(json) { dejar usuario = JSON.parse(json); si (!usuario.edad) { lanzar un nuevo PropertyRequiredError("edad"); } si (!nombre.usuario) { lanzar nuevo PropertyRequiredError("nombre"); } usuario que regresa; } // Ejemplo de trabajo con try..catch intentar { let usuario = readUser('{ "edad": 25 }'); } atrapar (errar) { if (err instancia de ValidationError) { alert("Datos no válidos: " + err.message); // Datos no válidos: Sin propiedad: nombre alerta(error.nombre); // PropiedadRequiredError alerta(err.propiedad); // nombre } else if (err instancia de error de sintaxis) { alert("Error de sintaxis JSON: " + err.message); } demás { tirar errar; // error desconocido, vuelve a lanzarlo } }
La nueva clase PropertyRequiredError
es fácil de usar: solo necesitamos pasar el nombre de la propiedad: new PropertyRequiredError(property)
. El message
legible por humanos es generado por el constructor.
Tenga en cuenta que this.name
en el constructor PropertyRequiredError
se asigna nuevamente manualmente. Esto puede resultar un poco tedioso: asignar this.name = <class name>
en cada clase de error personalizada. Podemos evitarlo creando nuestra propia clase de "error básico" que asigne this.name = this.constructor.name
. Y luego heredar todos nuestros errores personalizados.
Llamémoslo MyError
.
Aquí está el código con MyError
y otras clases de error personalizadas, simplificado:
clase MyError extiende Error { constructor(mensaje) { super(mensaje); este.nombre = este.constructor.nombre; } } clase ValidationError extiende MyError { } clase PropertyRequiredError extiende ValidationError { constructor(propiedad) { super("Sin propiedad: " + propiedad); esta.propiedad = propiedad; } } // el nombre es correcto alerta (nuevo PropertyRequiredError ("campo"). nombre); // PropiedadRequiredError
Ahora los errores personalizados son mucho más cortos, especialmente ValidationError
, ya que eliminamos la línea "this.name = ..."
en el constructor.
El propósito de la función readUser
en el código anterior es "leer los datos del usuario". Pueden ocurrir diferentes tipos de errores en el proceso. En este momento tenemos SyntaxError
y ValidationError
, pero en el futuro la función readUser
puede crecer y probablemente generar otros tipos de errores.
El código que llama readUser
debería manejar estos errores. En este momento utiliza múltiples if
en el bloque catch
, que verifican la clase y manejan los errores conocidos y vuelven a generar los desconocidos.
El esquema es así:
intentar { ... readUser() // la posible fuente de error ... } atrapar (errar) { if (err instancia de ValidationError) { // manejar errores de validación } else if (err instancia de error de sintaxis) { // manejar errores de sintaxis } demás { tirar errar; // error desconocido, vuelve a lanzarlo } }
En el código anterior podemos ver dos tipos de errores, pero puede haber más.
Si la función readUser
genera varios tipos de errores, entonces deberíamos preguntarnos: ¿realmente queremos verificar todos los tipos de errores uno por uno cada vez?
A menudo la respuesta es “No”: nos gustaría estar “un nivel por encima de todo eso”. Sólo queremos saber si hubo un “error de lectura de datos”; a menudo es irrelevante el motivo exacto (el mensaje de error lo describe). O, mejor aún, nos gustaría tener una manera de obtener los detalles del error, pero sólo si es necesario.
La técnica que describimos aquí se llama "ajustar excepciones".
Crearemos una nueva clase ReadError
para representar un error genérico de "lectura de datos".
La función readUser
detectará los errores de lectura de datos que ocurran dentro de ella, como ValidationError
y SyntaxError
, y generará un ReadError
en su lugar.
El objeto ReadError
mantendrá la referencia al error original en su propiedad cause
.
Entonces el código que llama readUser
solo tendrá que verificar ReadError
, no todo tipo de errores de lectura de datos. Y si necesita más detalles de un error, puede verificar su propiedad cause
.
Aquí está el código que define ReadError
y demuestra su uso en readUser
y try..catch
:
clase ReadError extiende Error { constructor(mensaje, causa) { super(mensaje); esta.causa = causa; this.name = 'Error de lectura'; } } clase ValidationError extiende Error { /*...*/ } clase PropertyRequiredError extiende ValidationError { /* ... */ } función validarUsuario(usuario) { si (!usuario.edad) { lanzar un nuevo PropertyRequiredError("edad"); } si (!nombre.usuario) { lanzar nuevo PropertyRequiredError("nombre"); } } función leerUsuario(json) { dejar usuario; intentar { usuario = JSON.parse(json); } atrapar (errar) { if (err instancia de SyntaxError) { lanzar un nuevo ReadError ("Error de sintaxis", err); } demás { tirar errar; } } intentar { validarUsuario(usuario); } atrapar (errar) { if (err instancia de ValidationError) { lanzar un nuevo ReadError ("Error de validación", err); } demás { tirar errar; } } } intentar { readUser('{json incorrecto}'); } atrapar (e) { si (e instancia de ReadError) { alerta(e); // Error original: SyntaxError: token b inesperado en JSON en la posición 1 alert("Error original: " + e.cause); } demás { tirar e; } }
En el código anterior, readUser
funciona exactamente como se describe: detecta errores de sintaxis y validación y, en su lugar, genera errores ReadError
(los errores desconocidos se vuelven a generar como de costumbre).
Entonces el código externo verifica instanceof ReadError
y eso es todo. No es necesario enumerar todos los tipos de errores posibles.
El enfoque se llama "envolver excepciones", porque tomamos excepciones de "bajo nivel" y las "envolvemos" en ReadError
, que es más abstracto. Es ampliamente utilizado en programación orientada a objetos.
Normalmente podemos heredar de Error
y otras clases de error integradas. Solo tenemos que cuidar la propiedad name
y no olvidarnos de llamar super
.
Podemos usar instanceof
para verificar errores particulares. También funciona con herencia. Pero a veces tenemos un objeto de error proveniente de una biblioteca de terceros y no hay una manera fácil de obtener su clase. Entonces la propiedad name
se puede utilizar para dichas comprobaciones.
Ajustar excepciones es una técnica muy extendida: una función maneja excepciones de bajo nivel y crea errores de nivel superior en lugar de varios errores de bajo nivel. Las excepciones de bajo nivel a veces se convierten en propiedades de ese objeto como err.cause
en los ejemplos anteriores, pero eso no es estrictamente necesario.
importancia: 5
Cree una clase FormatError
que herede de la clase SyntaxError
incorporada.
Debería admitir las propiedades de message
, name
y stack
.
Ejemplo de uso:
let err = new FormatError("error de formato"); alerta (err.mensaje); // error de formato alerta( error.nombre ); // Error de formato alerta (err.pila); // pila alerta (errar instancia de FormatError); // verdadero alerta (errar instancia de SyntaxError); // verdadero (porque hereda de SyntaxError)
clase FormatError extiende SyntaxError { constructor(mensaje) { super(mensaje); este.nombre = este.constructor.nombre; } } let err = new FormatError("error de formato"); alerta (err.mensaje); // error de formato alerta( error.nombre ); // Error de formato alerta (err.pila); // pila alerta (errar instancia de SyntaxError); // verdadero