Cuando desarrollamos algo, a menudo necesitamos nuestras propias clases de error para reflejar cosas especficas que pueden salir mal en nuestras tareas. Para errores en las operaciones de red, podemos necesitar HttpError, para las operaciones de la base de datos DbError, para las operaciones de bsqueda NotFoundError, etc.
Nuestros errores deben admitir propiedades de error bsicas como message, name y, preferiblemente, stack. Pero tambin pueden tener otras propiedades propias, por ejemplo, los objetos HttpError pueden tener una propiedad statusCode con un valor como 404 o 403 o 500.
JavaScript permite usar throw con cualquier argumento, por lo que tcnicamente nuestras clases de error personalizadas no necesitan heredarse de Error. Pero si heredamos, entonces es posible usar obj instanceof Error para identificar objetos error. Entonces es mejor heredar de l.
A medida que la aplicacin crece, nuestros propios errores forman naturalmente una jerarqua. Por ejemplo, HttpTimeoutError puede heredar de HttpError, y as sucesivamente.
Extendiendo Error
Como ejemplo, consideremos una funcin readUser(json) que debera leer JSON con los datos del usuario.
Aqu hay un ejemplo de cmo puede verse un json vlido:
let json = `{ "name": "John", "age": 30 }`;
Internamente, usaremos JSON.parse. Si recibe json mal formado, entonces arroja SyntaxError. Pero incluso si json es sintcticamente correcto, eso no significa que sea un usuario vlido, verdad? Puede perder los datos necesarios. Por ejemplo, puede no tener propiedades de nombre y edad que son esenciales para nuestros usuarios.
Nuestra funcin readUser(json) no solo leer JSON, sino que verificar (validar) los datos. Si no hay campos obligatorios, o el formato es incorrecto, entonces es un error. Y eso no es un SyntaxError, porque los datos son sintcticamente correctos, sino otro tipo de error. Lo llamaremos ValidationError y crearemos una clase para ello. Un error de ese tipo tambin debe llevar la informacin sobre el campo infractor.
Nuestra clase ValidationError debera heredar de la clase incorporada Error.
Esa clase est incorporada, pero aqu est su cdigo aproximado para que podamos entender lo que estamos extendiendo:
// El "pseudocdigo" para la clase Error incorporada definida por el propio JavaScript
class Error {
constructor(message) {
this.message = message;
this.name = "Error"; // (diferentes nombres para diferentes clases error incorporadas)
this.stack = <call stack>; // no estndar, pero la mayora de los entornos lo admiten
}
}
Ahora heredemos ValidationError y probmoslo en accin:
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 nmeros de lnea para cada una
}
Tenga en cuenta: en la lnea (1) llamamos al constructor padre. JavaScript requiere que llamemos super en el constructor hijo, por lo que es obligatorio. El constructor padre establece la propiedad message.
El constructor principal tambin establece la propiedad name en "Error", por lo que en la lnea (2) la restablecemos al valor correcto.
Intentemos usarlo en readUser(json):
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
// Uso
function 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..catch
try {
let user = readUser('{ "age": 25 }');
} catch (err) {
if (err instanceof ValidationError) {
alert("Dato invlido: " + err.message); // Dato invlido: sin campo: nombre
} else if (err instanceof SyntaxError) { // (*)
alert("Error de sintaxis JSON: " + err.message);
} else {
throw err; // error desconocido, vuelva a lanzarlo (**)
}
}
El bloque try..catch en el cdigo anterior maneja tanto nuestro ValidationError como el SyntaxError incorporado de JSON.parse.
Observe cmo usamos instanceof para verificar el tipo de error especfico en la lnea (*).
Tambin podramos mirar err.name, as:
// ...
// en lugar de (err instanceof SyntaxError)
} else if (err.name == "SyntaxError") { // (*)
// ...
La versin instanceof es mucho mejor, porque en el futuro vamos a extender ValidationError, haremos subtipos de ella, como PropertyRequiredError. Y el control instanceof continuar funcionando para las nuevas clases heredadas. Entonces eso es a prueba de futuro.
Tambin es importante que si catch encuentra un error desconocido, entonces lo vuelva a lanzar en la lnea (**). El bloque catch solo sabe cmo manejar los errores de validacin y sintaxis, otros tipos de error (como los tipogrficos en el cdigo u otros desconocidos) deben pasar a travs y ser relanzados.
Herencia adicional
La clase ValidationError es demasiado genrica. Son muchas las cosas que pueden salir mal. La propiedad podra estar ausente, o puede estar en un formato incorrecto (como un valor de cadena para age en lugar de un nmero). Hagamos una clase ms concreta PropertyRequiredError especficamente para propiedades ausentes. Esta clase llevar informacin adicional sobre la propiedad 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;
}
}
// Uso
function 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..catch
try {
let user = readUser('{ "age": 25 }');
} catch (err) {
if (err instanceof ValidationError) {
alert("Dato invlido: " + err.message); // Dato invlido: 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 nueva clase PropertyRequiredError es fcil de usar: solo necesitamos pasar el nombre de la propiedad: new PropertyRequiredError(property). El message legible para humanos es generado por el constructor.
Tenga en cuenta que this.name en el constructor PropertyRequiredError se asigna de nuevo manualmente. Eso puede volverse un poco tedioso: asignar this.name = <class name> en cada clase de error personalizada. Podemos evitarlo haciendo nuestra propia clase error bsico que asigna this.name = this.constructor.name. Y luego herede todos nuestros errores personalizados.
Llammosla MyError.
Aqu est el cdigo con MyError y otras clases error personalizadas, simplificadas:
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 incorrecto
alert( new PropertyRequiredError("campo").name ); // PropertyRequiredError
Ahora los errores personalizados son mucho ms cortos, especialmente ValidationError, ya que eliminamos la lnea "this.name = ..." en el constructor.
Empacado de Excepciones
El propsito de la funcin readUser en el cdigo anterior es leer los datos del usuario. Puede haber diferentes tipos de errores en el proceso. En este momento tenemos SyntaxError y ValidationError, pero en el futuro la funcin readUser puede crecer y probablemente generar otros tipos de errores.
El cdigo que llama a readUser debe manejar estos errores. En este momento utiliza mltiples if en el bloque catch, que verifican la clase y manejan los errores conocidos y vuelven a arrojar los desconocidos.
El esquema es as:
try {
...
readUser() // la fuente potencial de error
...
} catch (err) {
if (err instanceof ValidationError) {
// manejar errores de validacin
} else if (err instanceof SyntaxError) {
// manejar errores de sintaxis
} else {
throw err; // error desconocido, vuelva a lanzarlo
}
}
En el cdigo anterior podemos ver dos tipos de errores, pero puede haber ms.
Si la funcin readUser genera varios tipos de errores, entonces debemos preguntarnos: realmente queremos verificar todos los tipos de error uno por uno cada vez?
A menudo, la respuesta es No: nos gustara estar un nivel por encima de todo eso. Solo queremos saber si hubo un error de lectura de datos: el por qu ocurri exactamente es a menudo irrelevante (el mensaje de error lo describe). O, mejor an, nos gustara tener una forma de obtener los detalles del error, pero solo si es necesario.
La tcnica que describimos aqu se llama empacado de excepciones.
- Crearemos una nueva clase
ReadErrorpara representar un error genrico de lectura de datos. - La funcin
readUserdetectar los errores de lectura de datos que ocurren dentro de ella, comoValidationErrorySyntaxError, y generar unReadErroren su lugar. - El objeto
ReadErrormantendr la referencia al error original en su propiedadcause.
Entonces, el cdigo que llama a readUser solo tendr que verificar ReadError, no todos los tipos de errores de lectura de datos. Y si necesita ms detalles de un error, puede verificar su propiedad cause.
Aqu est el cdigo que define ReadError y demuestra su uso en readUser y 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 validacin", 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 posicin 1
alert("Error original: " + e.cause);
} else {
throw e;
}
}
En el cdigo anterior, readUser funciona exactamente como se describe: detecta los errores de sintaxis y validacin y arroja los errores ReadError en su lugar (los errores desconocidos se vuelven a generar como de costumbre).
Entonces, el cdigo externo verifica instanceof ReadError y eso es todo. No es necesario enumerar todos los tipos de error posibles.
El enfoque se llama empacado de excepciones, porque tomamos excepciones de bajo nivel y las ajustamos en ReadError que es ms abstracto. Es ampliamente utilizado en la programacin orientada a objetos.
Resumen
- Podemos heredar de
Errory otras clases de error incorporadas normalmente. Solo necesitamos cuidar la propiedadnamey no olvidemos llamarsuper. - Podemos usar
instanceofpara verificar errores particulares. Tambin funciona con herencia. Pero a veces tenemos un objeto error que proviene de una biblioteca de terceros y no hay una manera fcil de obtener su clase. Entonces la propiedadnamepuede usarse para tales controles. - Empacado de excepciones es una tcnica generalizada: una funcin maneja excepciones de bajo nivel y crea errores de alto nivel en lugar de varios errores de bajo nivel. Las excepciones de bajo nivel a veces se convierten en propiedades de ese objeto como
err.causeen los ejemplos anteriores, pero eso no es estrictamente necesario.
Comentarios
<code>, para varias lneas envolverlas en la etiqueta<pre>, para ms de 10 lneas utilice una entorno controlado (sandbox) (plnkr, jsbin, codepen)