Índice
- Errores operativos
- Errores del programador
- Patrón de manejo de errores incorrecto n.° 1: uso incorrecto de devoluciones de llamada
- Patrón de manejo de errores incorrecto n.° 2: uso incorrecto de promesas
- Prometedor de una API basada en devolución de llamada
- Ejemplos del mundo real
- Resumiendo las cosas negativas
- Clases de error
- Consejos para utilizar clases de error
- Conclusión
Este artículo está dirigido a desarrolladores de JavaScript y NodeJS que desean mejorar el manejo de errores en sus aplicaciones. Kelvin Omereshone explica el error
patrón de clases y cómo usarlo para una forma mejor y más eficiente de manejar los errores en sus aplicaciones.
El manejo de errores es una de esas partes del desarrollo de software que no recibe la atención que realmente merece. Sin embargo, crear aplicaciones sólidas requiere abordar los errores de forma adecuada.
Puede arreglárselas en NodeJS sin manejar adecuadamente los errores, pero debido a la naturaleza asincrónica de NodeJS, el manejo inadecuado o los errores pueden causarle problemas muy pronto, especialmente al depurar aplicaciones.
Antes de continuar, me gustaría señalar el tipo de errores que discutiremos y cómo utilizar las clases de error.
Errores operativos
Estos son errores descubiertos durante el tiempo de ejecución de un programa. Los errores operativos no son errores y pueden ocurrir de vez en cuando principalmente debido a uno o una combinación de varios factores externos, como el tiempo de espera del servidor de la base de datos o la decisión de un usuario de intentar la inyección SQL ingresando consultas SQL en un campo de entrada.
A continuación se muestran más ejemplos de errores operativos:
- No se pudo conectar a un servidor de base de datos;
- Entradas no válidas por parte del usuario (el servidor responde con un
400
código de respuesta); - Pide tiempo fuera;
- Recurso no encontrado (el servidor responde con un código de respuesta 404);
- El servidor regresa con una
500
respuesta.
También vale la pena comentar brevemente la contraparte de los errores operativos.
Errores del programador
Estos son errores en el programa que se pueden resolver cambiando el código. Este tipo de errores no se pueden manejar porque ocurren como resultado de la ruptura del código. Ejemplos de estos errores son:
- Intentando leer una propiedad en un objeto que no está definido.
const user = { firstName: 'Kelvin', lastName: 'Omereshone', } console.log(user.fullName) // throws 'undefined' because the property fullName is not defined
- Invocar o llamar a una función asincrónica sin devolución de llamada.
- Pasar una cadena donde se esperaba un número.
Este artículo trata sobre el manejo de errores operativos en NodeJS. El manejo de errores en NodeJS es significativamente diferente del manejo de errores en otros lenguajes. Esto se debe a la naturaleza asincrónica de JavaScript y a la apertura de JavaScript con errores. Dejame explicar:
En JavaScript, las instancias de la error
clase no son lo único que puedes lanzar. Literalmente, puede arrojar cualquier tipo de datos; esta apertura no está permitida en otros idiomas.
Por ejemplo, un desarrollador de JavaScript puede decidir incluir un número en lugar de una instancia de objeto de error, así:
// badthrow 'Whoops :)';// goodthrow new Error('Whoops :)')
Es posible que no vea el problema al generar otros tipos de datos, pero hacerlo resultará en una depuración más difícil porque no obtendrá un seguimiento de la pila ni otras propiedades que expone el objeto Error que son necesarias para la depuración.
Veamos algunos patrones incorrectos en el manejo de errores, antes de echar un vistazo al patrón de clase Error y cómo es una forma mucho mejor de manejar errores en NodeJS.
Patrón de manejo de errores incorrecto n.° 1: uso incorrecto de devoluciones de llamada
Escenario del mundo real : su código depende de una API externa que requiere una devolución de llamada para obtener el resultado que espera.
Tomemos el siguiente fragmento de código:
'use strict';const fs = require('fs');const write = function () { fs.mkdir('./writeFolder'); fs.writeFile('./writeFolder/foobar.txt', 'Hello World');}write();
Hasta NodeJS 8 y superiores, el código anterior era legítimo y los desarrolladores simplemente activaban y olvidaban los comandos. Esto significa que los desarrolladores no estaban obligados a proporcionar una devolución de llamada a dichas llamadas a funciones y, por lo tanto, podían omitir el manejo de errores. ¿Qué pasa cuando writeFolder
no se ha creado? La llamada writeFile
no se realizará y no sabremos nada al respecto. Esto también podría resultar en una condición de carrera porque es posible que el primer comando no haya terminado cuando el segundo comando comenzó nuevamente, no lo sabrías.
Comencemos a resolver este problema resolviendo la condición de carrera. Lo haríamos devolviendo la llamada al primer comando mkdir
para asegurarnos de que el directorio exista antes de escribir en él con el segundo comando. Entonces nuestro código se vería como el siguiente:
'use strict';const fs = require('fs');const write = function () { fs.mkdir('./writeFolder', () = { fs.writeFile('./writeFolder/foobar.txt', 'Hello World!'); });}write();
Aunque resolvimos la condición de carrera, aún no hemos terminado. Nuestro código sigue siendo problemático porque aunque usamos una devolución de llamada para el primer comando, no tenemos forma de saber si la carpeta writeFolder
se creó o no. Si la carpeta no se creó, la segunda llamada volverá a fallar, pero aun así ignoramos el error una vez más. Resolvemos esto por…
Manejo de errores con devoluciones de llamada
Para manejar correctamente los errores con las devoluciones de llamada, debe asegurarse de utilizar siempre el enfoque de error primero. Lo que esto significa es que primero debe verificar si la función devuelve un error antes de continuar con los datos (si los hay) que se devolvieron. Veamos la forma incorrecta de hacer esto:
'use strict';// Wrongconst fs = require('fs');const write = function (callback) { fs.mkdir('./writeFolder', (err, data) = { if (data) fs.writeFile('./writeFolder/foobar.txt', 'Hello World!'); else callback(err) });}write(console.log);
El patrón anterior es incorrecto porque a veces la API a la que llama puede no devolver ningún valor o puede devolver un valor falso como valor de retorno válido. Esto le haría terminar en un caso de error aunque aparentemente haya realizado una llamada exitosa a la función o API.
El patrón anterior también es malo porque su uso consumiría su error (sus errores no serán llamados aunque haya sucedido). Tampoco tendrá idea de lo que sucede en su código como resultado de este tipo de patrón de manejo de errores. Entonces la forma correcta para el código anterior sería:
'use strict';// Rightconst fs = require('fs');const write = function (callback) { fs.mkdir('./writeFolder', (err, data) = { if (err) return callback(err) fs.writeFile('./writeFolder/foobar.txt', 'Hello World!'); });}write(console.log);
Patrón de manejo de errores incorrecto n.° 2: uso incorrecto de promesas
Escenario del mundo real : Entonces descubriste las Promesas y crees que son mucho mejores que las devoluciones de llamada debido al infierno de las devoluciones de llamadas y decidiste prometer alguna API externa de la que dependía tu base de código. O está consumiendo una promesa de una API externa o una API del navegador como la función fetch().
En estos días realmente no usamos devoluciones de llamada en nuestras bases de código NodeJS, usamos promesas . Así que volvamos a implementar nuestro código de ejemplo con una promesa:
'use strict';const fs = require('fs').promises;const write = function () { return fs.mkdir('./writeFolder').then(() = { fs.writeFile('./writeFolder/foobar.txt', 'Hello world!') }).catch((err) = { // catch all potential errors console.error(err) })}
Pongamos el código anterior bajo un microscopio: podemos ver que estamos bifurcando la fs.mkdir
promesa en otra cadena de promesa (la llamada a fs.writeFile) sin siquiera manejar esa llamada de promesa. Podrías pensar que una mejor manera de hacerlo sería:
'use strict';const fs = require('fs').promises;const write = function () { return fs.mkdir('./writeFolder').then(() = { fs.writeFile('./writeFolder/foobar.txt', 'Hello world!').then(() = { // do something }).catch((err) = { console.error(err); }) }).catch((err) = { // catch all potential errors console.error(err) })}
Pero lo anterior no escalaría. Esto se debe a que si tenemos más cadenas de promesas para llamar, terminaríamos con algo similar al infierno de devolución de llamadas cuya solución se hicieron las promesas. Esto significa que nuestro código seguirá sangrando hacia la derecha. Tendríamos un infierno de promesas en nuestras manos.
Prometedor de una API basada en devolución de llamada
La mayoría de las veces querrás prometer una API basada en devolución de llamada por tu cuenta para poder manejar mejor los errores en esa API. Sin embargo, esto no es realmente fácil de hacer. Tomemos un ejemplo a continuación para explicar por qué.
function doesWillNotAlwaysSettle(arg) { return new Promise((resolve, reject) = { doATask(foo, (err) = { if (err) { return reject(err); } if (arg === true) { resolve('I am Done') } }); });}
De lo anterior, si arg
no es así true
y no tenemos un error en la llamada a la doATask
función, entonces esta promesa simplemente se colgará, lo cual es una pérdida de memoria en su aplicación.
Errores de sincronización ingeridos en promesas
El uso del constructor Promise tiene varias dificultades, una de estas dificultades es; tan pronto como se resuelve o se rechaza, no puede obtener otro estado. Esto se debe a que una promesa solo puede obtener un único estado: pendiente o resuelta/rechazada. Esto significa que podemos tener zonas muertas en nuestras promesas. Veamos esto en código:
function deadZonePromise(arg) { return new Promise((resolve, reject) = { doATask(foo, (err) = { resolve('I’m all Done'); throw new Error('I am never reached') // Dead Zone }); });}
De lo anterior vemos que tan pronto como se resuelve la promesa, la siguiente línea es una zona muerta y nunca se alcanzará. Esto significa que cualquier siguiente manejo de errores sincrónicos realizado en sus promesas simplemente será absorbido y nunca será descartado.
Ejemplos del mundo real
Los ejemplos anteriores ayudan a explicar los patrones deficientes de manejo de errores; echemos un vistazo al tipo de problemas que puede ver en la vida real.
Ejemplo del mundo real n.° 1: transformación del error en cadena
Escenario : decidió que el error devuelto por una API no es lo suficientemente bueno para usted, por lo que decidió agregarle su propio mensaje. Blog sobre termux
'use strict';function readTemplate() { return new Promise(() = { databaseGet('query', function(err, data) { if (err) { reject('Template not found. Error: ', + err); } else { resolve(data); } }); });}readTemplate();
Veamos qué está mal con el código anterior. De lo anterior vemos que el desarrollador está intentando mejorar el error arrojado por la databaseGet
API concatenando el error devuelto con la cadena "Plantilla no encontrada". Este enfoque tiene muchas desventajas porque cuando se realiza la concatenación, el desarrollador ejecuta implícitamente toString
el objeto de error devuelto. De esta manera, pierde cualquier información adicional devuelta por el error (dígale adiós al seguimiento de la pila). Entonces, lo que el desarrollador tiene ahora es solo una cadena que no es útil al depurar.
Una mejor manera es mantener el error tal como está o envolverlo en otro error que haya creado y adjuntar el error arrojado desde la llamada a get de la base de datos como una propiedad.
Ejemplo del mundo real n.º 2: ignorar completamente el error
Escenario : tal vez cuando un usuario se registra en su aplicación, si ocurre un error, simplemente desea detectar el error y mostrar un mensaje personalizado, pero ignoró por completo el error que se detectó sin siquiera registrarlo para fines de depuración.
router.get('/:id', function (req, res, next) { database.getData(req.params.userId) .then(function (data) { if (data.length) { res.status(200).json(data); } else { res.status(404).end(); } }) .catch(() = { log.error('db.rest/get: could not get data: ', req.params.userId); res.status(500).json({error: 'Internal server error'}); })});
De lo anterior, podemos ver que el error se ignora por completo y el código envía 500 al usuario si falla la llamada a la base de datos. Pero en realidad, la causa del fallo de la base de datos podría ser datos con formato incorrecto enviados por el usuario, que es un error con el código de estado 400.
En el caso anterior, terminaríamos en un horror de depuración porque usted, como desarrollador, no sabría qué salió mal. El usuario no podrá dar un informe decente porque siempre se produce un error 500 interno del servidor. Terminaría perdiendo horas buscando el problema, lo que equivaldría a una pérdida de tiempo y dinero para su empleador.
Ejemplo del mundo real n.° 3: no aceptar el error generado desde una API
Escenario : se arrojó un error desde una API que estaba utilizando, pero no acepta ese error, en lugar de eso, organiza y transforma el error de manera que lo haga inútil para fines de depuración.
Tome el siguiente ejemplo de código a continuación:
async function doThings(input) { try { validate(input); try { await db.create(input); } catch (error) { error.message = `Inner error: ${error.message}` if (error instanceof Klass) { error.isKlass = true; } throw error } } catch (error) { error.message = `Could not do things: ${error.message}`; await rollback(input); throw error; }}
Están sucediendo muchas cosas en el código anterior que llevarían al horror de la depuración. Vamos a ver:
- Envolviendo
try/catch
bloques: puedes ver en lo anterior que estamos envolviendotry/catch
bloques, lo cual es una muy mala idea. Normalmente intentamos reducir el uso detry/catch
bloques para minimizar la superficie donde tendríamos que manejar nuestro error (piense en ello como manejo de errores SECO); - También estamos manipulando el mensaje de error en un intento de mejorar, lo cual tampoco es una buena idea;
- Estamos verificando si el error es una instancia de tipo
Klass
y, en este caso, estamos configurando una propiedad booleana del errorisKlass
en truev (pero si esa verificación pasa, entonces el error es del tipoKlass
); - También estamos revirtiendo la base de datos demasiado pronto porque, desde la estructura del código, existe una alta tendencia a que ni siquiera hayamos accedido a la base de datos cuando se produjo el error.
A continuación se muestra una mejor manera de escribir el código anterior:
async function doThings(input) { validate(input); try { await db.create(input); } catch (error) { try { await rollback(); } catch (error) { logger.log('Rollback failed', error, 'input:', input); } throw error; }}
Analicemos lo que estamos haciendo correctamente en el fragmento anterior:
- Estamos usando un
try/catch
bloque y solo en el bloque catch estamos usando otrotry/catch
bloque que sirve como guardia en caso de que algo suceda con esa función de reversión y lo estemos registrando; - Finalmente, arrojamos nuestro error de recepción original, lo que significa que no perdemos el mensaje incluido en ese error.
Pruebas
Principalmente queremos probar nuestro código (ya sea manual o automáticamente). Pero la mayoría de las veces sólo estamos probando las cosas positivas. Para una prueba sólida, también debe probar errores y casos extremos. Esta negligencia es responsable de que los errores lleguen a la producción, lo que costaría más tiempo adicional de depuración.
Consejo : asegúrese siempre de probar no solo los aspectos positivos (obtener un código de estado de 200 desde un punto final) sino también todos los casos de error y también todos los casos extremos.
Ejemplo del mundo real n.° 4: rechazos no controlados
Si ha usado promesas antes, probablemente se haya topado con unhandled rejections
.
A continuación se ofrece una introducción rápida a los rechazos no gestionados. Los rechazos no gestionados son rechazos de promesas que no se gestionaron. Esto significa que la promesa fue rechazada pero su código seguirá ejecutándose.
Veamos un ejemplo común del mundo real que conduce a rechazos no controlados.
'use strict';async function foobar() { throw new Error('foobar');}async function baz() { throw new Error('baz')}(async function doThings() { const a = foobar(); const b = baz(); try { await a; await b; } catch (error) { // ignore all errors! }})();
A primera vista, el código anterior puede no parecer propenso a errores. Pero si lo miramos más de cerca, empezamos a ver un defecto. Me explico: ¿Qué pasa cuando a
es rechazado? Ese medio await b
nunca se alcanza y eso significa que es un rechazo no controlado. Una posible solución es utilizar Promise.all
ambas promesas. Entonces el código se leería así:
'use strict';async function foobar() { throw new Error('foobar');}async function baz() { throw new Error('baz')}(async function doThings() { const a = foobar(); const b = baz(); try { await Promise.all([a, b]); } catch (error) { // ignore all errors! }})();
Aquí hay otro escenario del mundo real que conduciría a un error de rechazo de promesa no controlado:
'use strict';async function foobar() { throw new Error('foobar');}async function doThings() { try { return foobar() } catch { // ignoring errors again ! }}doThings();
Si ejecuta el fragmento de código anterior, obtendrá un rechazo de promesa no controlada, y este es el motivo: aunque no es obvio, devolvemos una promesa (foobar) antes de manejarla con el archivo try/catch
. Lo que debemos hacer es esperar la promesa que estamos manejando con el try/catch
para que el código diga:
'use strict';async function foobar() { throw new Error('foobar');}async function doThings() { try { return await foobar() } catch { // ignoring errors again ! }}doThings();
Resumiendo las cosas negativas
Ahora que ha visto patrones de manejo de errores incorrectos y posibles soluciones, profundicemos en el patrón de clase Error y cómo resuelve el problema del manejo de errores incorrecto en NodeJS.
Clases de error
En este patrón, comenzaríamos nuestra aplicación con una ApplicationError
clase de esta manera sabemos que todos los errores en nuestras aplicaciones que arrojemos explícitamente heredarán de ella. Entonces comenzaríamos con las siguientes clases de error:
ApplicationError
Este es el antepasado de todas las demás clases de error, es decir, todas las demás clases de error heredan de él.DatabaseError
Cualquier error relacionado con las operaciones de la base de datos se heredará de esta clase.UserFacingError
Cualquier error producido como resultado de la interacción de un usuario con la aplicación se heredaría de esta clase.
Así es como error
se vería nuestro archivo de clase:
'use strict';// Here is the base error classes to extend fromclass ApplicationError extends Error { get name() { return this.constructor.name; }}class DatabaseError extends ApplicationError { }class UserFacingError extends ApplicationError { }module.exports = { ApplicationError, DatabaseError, UserFacingError}
Este enfoque nos permite distinguir los errores arrojados por nuestra aplicación. Entonces, ahora, si queremos manejar un error de solicitud incorrecto (entrada de usuario no válida) o un error no encontrado (recurso no encontrado), podemos heredar de la clase base que es ( UserFacingError
como en el código siguiente).
const { UserFacingError } = require('./baseErrors')class BadRequestError extends UserFacingError { constructor(message, options = {}) { super(message); // You can attach relevant information to the error instance // (e.g.. the username) for (const [key, value] of Object.entries(options)) { this[key] = value; } } get statusCode() { return 400; }}class NotFoundError extends UserFacingError { constructor(message, options = {}) { super(message); // You can attach relevant information to the error instance // (e.g.. the username) for (const [key, value] of Object.entries(options)) { this[key] = value; } } get statusCode() { return 404 }}module.exports = { BadRequestError, NotFoundError}
Uno de los beneficios del error
enfoque de clases es que si arrojamos uno de estos errores, por ejemplo, un NotFoundError
, cada desarrollador que lea este código base podrá comprender lo que está sucediendo en este momento (si leen el código).
También podrá pasar múltiples propiedades específicas para cada clase de error durante la creación de instancias de ese error.
Otro beneficio clave es que puede tener propiedades que siempre forman parte de una clase de error; por ejemplo, si recibe un error de cara al usuario, sabrá que un código de estado siempre forma parte de esta clase de error, ahora puede usarlo directamente en el código más adelante.
Consejos para utilizar clases de error
- Cree su propio módulo (posiblemente uno privado) para cada clase de error, de esa manera puede simplemente importarlo en su aplicación y usarlo en todas partes.
- Lanza solo los errores que te interesan (errores que son instancias de tus clases de error). De esta manera, sabrá que sus clases de error son su única fuente de verdad y contienen toda la información necesaria para depurar su aplicación.
- Tener un módulo de error abstracto es bastante útil porque ahora sabemos que toda la información necesaria sobre los errores que nuestras aplicaciones pueden generar está en un solo lugar.
- Manejar errores en capas. Si maneja errores en todas partes, tendrá un enfoque inconsistente para el manejo de errores del cual es difícil realizar un seguimiento. Por capas me refiero a bases de datos, capas express/fastify/HTTP, etc.
Veamos cómo se ven las clases de error en el código. Aquí hay un ejemplo en express:
const { DatabaseError } = require('./error')const { NotFoundError } = require('./userFacingErrors')const { UserFacingError } = require('./error')// Expressapp.get('/:id', async function (req, res, next) { let data try { data = await database.getData(req.params.userId) } catch (err) { return next(err); } if (!data.length) { return next(new NotFoundError('Dataset not found')); } res.status(200).json(data)})app.use(function (err, req, res, next) { if (err instanceof UserFacingError) { res.sendStatus(err.statusCode); // or res.status(err.statusCode).send(err.errorCode) } else { res.sendStatus(500) } // do your logic logger.error(err, 'Parameters: ', req.params, 'User data: ', req.user)});
De lo anterior, aprovechamos que Express expone un controlador de errores global que le permite manejar todos sus errores en un solo lugar. Puede ver la llamada next()
en los lugares donde estamos manejando errores. Esta llamada pasaría los errores al controlador que se define en la app.use
sección. Debido a que express no admite async/await, estamos usando try/catch
bloques.
Entonces, a partir del código anterior, para manejar nuestros errores solo necesitamos verificar si el error que se lanzó es una UserFacingError
instancia y automáticamente sabemos que habrá un código de estado en el objeto de error y se lo enviamos al usuario (es posible que desee también tiene un código de error específico que puede pasar al cliente) y eso es todo.
También notarás que en este patrón ( error
patrón de clase) cualquier otro error que no hayas arrojado explícitamente es un 500
error porque es algo inesperado que significa que no arrojaste explícitamente ese error en tu aplicación. De esta manera, podemos distinguir los tipos de errores que ocurren en nuestras aplicaciones.
Conclusión
El manejo adecuado de errores en su aplicación puede hacer que duerma mejor por la noche y ahorrar tiempo de depuración. Aquí hay algunos puntos clave que se pueden extraer de este artículo:
- Utilice clases de error configuradas específicamente para su aplicación;
- Implementar controladores de errores abstractos;
- Utilice siempre async/await;
- Hacer que los errores sean expresivos;
- El usuario promete si es necesario;
- Devolver estados y códigos de error adecuados;
- Utilice ganchos de promesa.
Útiles bits de front-end y UX, entregados una vez por semana.
Con herramientas que le ayudarán a realizar mejor su trabajo. Suscríbase y obtenga el PDF de las listas de verificación de diseño de interfaz inteligente de Vitaly por correo electrónico.
En front-end y UX . Con la confianza de más de 207.000 personas.
(ra, yk, il)Explora más en
- javascript
- Nodo.js
- Aplicaciones
Tal vez te puede interesar:
- ¿Deberían abrirse los enlaces en ventanas nuevas?
- 24 excelentes tutoriales de AJAX
- 70 técnicas nuevas y útiles de AJAX y JavaScript
- Más de 45 excelentes recursos y repositorios de fragmentos de código
Mejor manejo de errores en NodeJS con clases de error
Errores operativosErrores del programadorPatrón de manejo de errores incorrecto n.° 1: uso incorrecto de devoluciones de llamadaPatrón de manejo de errores
programar
es
https://aprendeprogramando.es/static/images/programar-mejor-manejo-de-errores-en-nodejs-con-clases-de-error-1053-0.jpg
2024-12-03
Si crees que alguno de los contenidos (texto, imagenes o multimedia) en esta página infringe tus derechos relativos a propiedad intelectual, marcas registradas o cualquier otro de tus derechos, por favor ponte en contacto con nosotros en el mail [email protected] y retiraremos este contenido inmediatamente