Errores personalizados y cómo extender Error
Cuando desarrollamos aplicaciones, a menudo necesitamos definir nuestras propias clases de error para reflejar problemas específicos que pueden surgir en nuestras tareas. Por ejemplo, para problemas en las operaciones de red podríamos necesitar un HttpError
, para operaciones de base de datos un DbError
, y para búsquedas fallidas un NotFoundError
, entre otros.
Nuestros errores personalizados deberían incluir propiedades básicas como message
, name
, y preferentemente stack
. Además, pueden tener propiedades adicionales propias, como statusCode
en HttpError
con valores como 404, 403 o 500.
Extender Error
Aunque en JavaScript se puede usar throw
con cualquier objeto, es preferible que nuestras clases de error hereden de Error
. De esta manera, podemos utilizar obj instanceof Error
para identificar objetos de error, lo cual es una práctica recomendada.
A medida que nuestras aplicaciones crecen, nuestros errores personalizados tienden a formar una jerarquía natural. Por ejemplo, HttpTimeoutError
puede heredar de HttpError
.
Veamos un ejemplo de cómo extender Error
creando una clase ValidationError
:
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
function test() {
throw new ValidationError("¡Algo salió mal!");
}
try {
test();
} catch (err) {
console.log(err.message); // ¡Algo salió mal!
console.log(err.name); // ValidationError
console.log(err.stack); // Rastreo de pila
}
En el constructor de ValidationError
, llamamos a super(message)
para inicializar la propiedad message
y luego asignamos this.name
al valor correcto.
Uso en una función de ejemplo
Consideremos una función readUser(json)
que debería leer y validar un objeto JSON con los datos del usuario. Aquí hay un ejemplo de un JSON válido:
let json = `{ "name": "John", "age": 30 }`;
Internamente, usaremos JSON.parse
para parsear el JSON. Si el JSON está mal formado, JSON.parse
lanzará un SyntaxError
. Pero incluso si el JSON es sintácticamente correcto, los datos pueden no ser válidos. Por ejemplo, pueden faltar campos obligatorios como name
y age
.
Nuestra función readUser(json)
no solo parseará el JSON, sino que también validará los datos. Si faltan campos obligatorios, lanzará un ValidationError
.
Aquí está el código de ValidationError
y readUser
:
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
function readUser(json) {
let user = JSON.parse(json);
if (!user.age) {
throw new ValidationError("Falta el campo: age");
}
if (!user.name) {
throw new ValidationError("Falta el campo: name");
}
return user;
}
try {
let user = readUser('{ "age": 25 }');
} catch (err) {
if (err instanceof ValidationError) {
console.log("Datos inválidos: " + err.message); // Datos inválidos: Falta el campo: name
} else if (err instanceof SyntaxError) {
console.log("Error de sintaxis en JSON: " + err.message);
} else {
throw err; // Relanzar error desconocido
}
}
Herencia adicional
La clase ValidationError
es bastante genérica. Vamos a crear una clase más específica PropertyRequiredError
para los casos en los que faltan propiedades específicas:
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
class PropertyRequiredError extends ValidationError {
constructor(property) {
super("Falta la propiedad: " + property);
this.name = "PropertyRequiredError";
this.property = property;
}
}
function readUser(json) {
let user = JSON.parse(json);
if (!user.age) {
throw new PropertyRequiredError("age");
}
if (!user.name) {
throw new PropertyRequiredError("name");
}
return user;
}
try {
let user = readUser('{ "age": 25 }');
} catch (err) {
if (err instanceof ValidationError) {
console.log("Datos inválidos: " + err.message); // Datos inválidos: Falta la propiedad: name
console.log(err.name); // PropertyRequiredError
console.log(err.property); // name
} else if (err instanceof SyntaxError) {
console.log("Error de sintaxis en JSON: " + err.message);
} else {
throw err; // Relanzar error desconocido
}
}
Empacado de excepciones
El propósito de readUser
es leer y validar datos del usuario. A medida que la función crece, puede generar múltiples tipos de errores. Para simplificar el manejo de errores, podemos empaquetar excepciones en una clase ReadError
:
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 malformado}');
} catch (e) {
if (e instanceof ReadError) {
console.log(e);
console.log("Error original: " + e.cause);
} else {
throw e;
}
}
En este código, readUser
detecta errores de sintaxis y validación, y lanza errores ReadError
en su lugar. El código que llama a readUser
solo necesita manejar ReadError
, lo cual simplifica significativamente el manejo de errores.
Resumen
- Podemos extender
Error
y otras clases de error nativas. - Es crucial usar
instanceof
para verificar tipos de error específicos. - La técnica de empacado de excepciones permite manejar errores de bajo nivel empaquetándolos en errores de nivel superior.
Este enfoque ayuda a mantener el código limpio y manejable a medida que crecen nuestras aplicaciones.