El Error
integrado en Javascript proporciona información útil, pero a menudo puede parecer carente de claridad. Una talla única es ideal para el idioma, pero podemos hacerlo mejor en nuestras propias aplicaciones. Ahí es donde entran en juego los errores personalizados.
Es posible que haya visto errores personalizados al usar Node.js. Tipos de error integrados en el nodo como AssertionError
, RangeError
, ReferenceError
, SyntaxError
, y SystemError
son todas extensiones de la clase nativa Error
.
Usar Error
como base le da toda la potencia de la implementación de Javascript más beneficios adicionales. Estos son solo algunos:
- Formatos de mensaje de error personalizados.
- Propiedades adicionales en el objeto Error.
- Errores con nombre para facilitar la depuración y el manejo de errores condicionales.
- Errores consistentes para que los consumidores de bibliotecas hagan referencia.
Extendiendo la clase de error
Para comenzar, usemos un ejemplo genérico. Extenderemos la clase Error
y usaremos super
para heredar sus funciones.
class CustomError extends Error { constructor(...params) { super(...params) // We're spreading `params` as a way to bring all of `Error`'s functionality in. }}
Ahora, cuando throw
un error, puede hacerlo con throw new CustomError("Something went wrong...")
. También le brinda la capacidad de verificar los errores con el tipo:
try { throw new CustomError("Something went wrong")} catch (error) { if (error instance of CustomError) { // do something specifically for that type of error }}
Esto por sí solo no hace mucho, aparte de darle un nuevo tipo de error para llamar y verificar. Para entender mejor lo que se puede hacer, veamos lo que viene estándar en Error
.
- Error.name Error
- .mensaje
- Error.prototipo.Error toString ()
- .prototipo.constructor()
No hay mucho con que trabajar, ¿verdad? Aparte del constructor y el método toString
, el nombre del error y el mensaje que describe el error son las únicas partes que probablemente usaremos. En el ejemplo anterior, message
se establece pasando «algo salió mal» como argumento al instanciar el error.
Afortunadamente, la mayoría de las plataformas javascript como el navegador y el nodo.js ha añadido sus propios métodos y propiedades encima de los listados. Nos centraremos en algunos aspectos de la implementación de errores de V8, el motor Javascript que impulsa Chrome y Node.js.
Las dos áreas de interés son stack
y captureStackTrace
. Como sus nombres sugieren, le permiten emerger el rastro de la pila. Veamos cómo se ve con un ejemplo.
class CustomError extends Error { constructor(...args) { super(...args) if (Error.captureStackTrace) { Error.captureStackTrace(this, CustomError) } this.name = "Our Custom Error" }}try { throw new CustomError("Something went wrong")} catch (err) { console.error(err.stack)}
En este ejemplo, llamamos a Error.captureStackTrace
, si la plataforma lo admite, para garantizar que se agregue un seguimiento completo a nuestro error personalizado. Luego throw
el error desde el bloque try
, que pasará el control al bloque catch
.
Si ejecuta el código anterior, verá que la primera línea de stack
es name
y message
del error.
Our Custom Error: Something went wrong
Este error no es muy «profundo», por lo que la pila es principalmente Nodo.js internos. Cada línea es un «marco» de la pila. Contienen detalles sobre la ubicación del error dentro de la base de código.
Ahora que sabemos cómo hacer y llamar un error personalizado, hagámoslo útil.
Personalizar contenido de error
El error personalizado con el que hemos estado trabajando está bien, pero cambiémoslo para que sea más útil. La clase será CircuitError
. Lo usaremos para proporcionar errores personalizados provenientes de un disyuntor. Si no estás familiarizado con el patrón, está bien. Los detalles de implementación del disyuntor no son importantes. Lo importante es que podamos pasar alguna información al error, y éste presentará esa información al usuario.
Un interruptor tiene un estado «ABIERTO» que no permite que nada pase a través de él durante un tiempo fijo. Vamos a configurar nuestro CircuitError
para que contenga algunos detalles que podrían ser útiles para las funciones que lo reciben cuando el estado es OPEN
.
class CircuitError extends Error { // 1 constructor(state, nextAttempt, ...params) { super(...params) if (Error.captureStackTrace) { Error.captureStackTrace(this, CircuitError) } this.name = "CircuitError" this.state = state // 2 this.message = `The Circuit is ${state}.` // 3 if (nextAttempt) { this.timestamp = Date.now() this.nextAttempt = nextAttempt this.message += ` Next attempt can be made in ${this.nextAttempt - this.timestamp}ms.` } }}
Nuestro error actualizado hace algunas cosas nuevas, todas dentro del constructor. Los argumentos pasados han cambiado (1) para incluir el state
del disyuntor, así como una marca de tiempo nextAttempt
para indicar cuándo el disyuntor comenzará a intentar solicitudes de nuevo.
A continuación, establece algunas propiedades nuevas y actualiza el mensaje en función de los valores presentados. Podemos probarlo lanzando una nueva versión de este error.
try { throw new CircuitError("OPEN", Date.now() + 8000)} catch (err) { console.error(err)}
Ahora, cuando lanzamos el error, toma el estado como primer argumento y una marca de tiempo como segundo. Para esta demostración estamos pasando en el tiempo 8000 milisegundos en el futuro. Observe que también estamos registrando el error en sí, en lugar de solo la pila.
Ejecutar el código resultará en algo como lo siguiente:
CircuitError : The Circuit is OPEN. Next attempt can be made in 6000ms. at Object.<anonymous> (/example.js:21:9) at Module._compile (internal/modules/cjs/loader.js:1063:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:1103:10) at Module.load (internal/modules/cjs/loader.js:914:32) at Function.Module._load (internal/modules/cjs/loader.js:822:14) at Function.Module.runMain (internal/modules/cjs/loader.js:1143:12) at internal/main/run_main_module.js:16:11 { name: 'Circuit Error', state: 'OPEN', message: 'The Circuit is OPEN. Next attempt can be made in 6000ms.', timestamp: 1580242308919, nextAttempt: 1580242314919}
El resultado es el nombre del error seguido del mensaje en la primera línea. Luego tenemos el resto de la pila, seguido de todas las propiedades que hemos establecido en CircuitError
. Nuestros consumidores de este error podrían usar esos datos para reaccionar al error. Por ejemplo:
try { throw new CircuitError("OPEN", Date.now() + 8000)} catch (err) { customLogger(err) if (err.state === "OPEN") { handleOpenState(err.nextAttempt) }}
En lugar de un error genérico, ahora tenemos algo que se adapta mejor a las necesidades de nuestra aplicación.
Reaccionar ante fallos
Los errores personalizados son una excelente manera de reaccionar ante fallos. Es común asumir que todos los errores son inesperados, pero al proporcionar errores útiles, puede hacer que su existencia sea más fácil de manejar. Al hacer uso de errores personalizados, no solo podemos proporcionar a los usuarios de nuestras aplicaciones datos valiosos, sino que también presentamos la capacidad de responder a los tipos de error a través de la condición instance of
.
En Bearer, utilizamos errores personalizados para estandarizar respuestas en nuestro código, proporcionar solo la información que necesita en nuestro cliente y hacer que nuestros errores sean procesables.
¿Cómo utiliza los errores personalizados en sus aplicaciones? Conéctese con nosotros @BearerSH y háganoslo saber.