Cómo hacer una promesa a partir de una función de devolución de llamada en JavaScript

Los desarrolladores de back-end se encuentran con desafíos todo el tiempo mientras crean aplicaciones o prueban código. Como desarrollador bastante nuevo y familiarizado con esos desafíos, nunca me he encontrado con un desafío o inconveniente con más frecuencia, o más memorable, que con las funciones de devolución de llamada.

No voy a profundizar demasiado en los detalles de la devolución de llamada y sus pros y contras o alternativas como promesas y async / await. Para obtener una explicación más vívida, puede consultar este artículo que los explica a fondo.

Infierno de devolución de llamada

Las devoluciones de llamada son una característica útil de JavaScript que le permite realizar llamadas asincrónicas. Son funciones que generalmente se pasan como un segundo parámetro a otra función que está recuperando datos o realizando una operación de E / S que requiere tiempo para completarse.

Por ejemplo, intente realizar una llamada a la API con el requestmódulo o conectarse a una base de datos MongoDB. Pero, ¿y si ambas llamadas dependen la una de la otra? ¿Qué sucede si los datos que está obteniendo son la URL de MongoDB a la que necesita conectarse?

Tendría que anidar estas llamadas una dentro de la otra:

request.get(url, function(error, response, mongoUrl) { if(error) throw new Error("Error while fetching fetching data"); MongoClient.connect(mongoUrl, function(error, client) { if(error) throw new Error("MongoDB connection error"); console.log("Connected successfully to server"); const db = client.db("dbName"); // Do some application logic client.close(); }); });

Está bien ... entonces, ¿dónde está el problema? Bueno, por un lado, la legibilidad del código se ve afectada por esta técnica.

Puede parecer correcto al principio cuando el código base es pequeño. Pero esto no se escala bien, especialmente si profundiza más capas en las devoluciones de llamada anidadas.

Terminará con una gran cantidad de corchetes de cierre y llaves que lo confundirán a usted y a otros desarrolladores sin importar qué tan bien formateado esté su código. Existe un sitio web llamado callbackhell que aborda este problema específico.

Escuché a algunos de ustedes, incluido mi ingenuo yo pasado, diciéndome que lo envuelva en un asyncfunción luego await la función de devolución de llamada. Esto simplemente no funciona.

Si hay algún bloque de código después de la función que usa devoluciones de llamada, ese bloque de código se ejecutará y NO esperará la devolución de llamada.

Aquí está el error que cometí antes:

var request = require('request'); // WRONG async function(){ let joke; let url = "//api.chucknorris.io/jokes/random" await request.get(url, function(error, response, data) { if(error) throw new Error("Error while fetching fetching data"); let content = JSON.parse(data); joke = content.value; }); console.log(joke); // undefined }; // Wrong async function(){ let joke; let url = "//api.chucknorris.io/jokes/random" request.get(url, await function(error, response, data) { if(error) throw new Error("Error while fetching fetching data"); let content = JSON.parse(data); joke = content.value; }); console.log(joke); // undefined };

Algunos desarrolladores más experimentados podrían decir "Simplemente use una biblioteca diferente que use promesas para hacer lo mismo, como axios,o simplemente use buscar ” . Claro que puedo en ese escenario, pero eso es simplemente huir del problema.

Además, esto es solo un ejemplo. A veces, puede verse obligado a usar una biblioteca que no admite promesas sin alternativas. Como usar kits de desarrollo de software (SDK) para comunicarse con plataformas como Amazon Web Services (AWS), Twitter o Facebook.

A veces, incluso usar una devolución de llamada para hacer una llamada muy simple con una operación rápida de E / S o CRUD está bien, y ninguna otra lógica depende de sus resultados. Pero es posible que esté limitado por el entorno de ejecución como en una función Lambda que mataría todos los procesos una vez que finalice el hilo principal, independientemente de las llamadas asincrónicas que no se hayan completado.

Solución 1 (fácil): use el módulo "util" de Node

La solución es sorprendentemente sencilla. Incluso si se siente un poco incómodo con la idea de las promesas en JavaScript, le encantará cómo puede resolver este problema usándolas.

Como señalaron Erop y Robin en los comentarios, la versión 8 de Nodejs y superior ahora admiten convertir las funciones de devolución de llamada en promesas utilizando el módulo util integrado .

const request = require('request'); const util = require('util'); const url = "//api.chucknorris.io/jokes/random"; // Use the util to promisify the request method const getChuckNorrisFact = util.promisify(request); // Use the new method to call the API in a modern then/catch pattern getChuckNorrisFact(url).then(data => { let content = JSON.parse(data.body); console.log('Joke: ', content.value); }).catch(err => console.log('error: ', err))

El código anterior resuelve el problema perfectamente usando el util.promisifymétodo disponible en la biblioteca principal de nodejs.

Todo lo que tiene que hacer es usar la función de devolución de llamada como argumento para util.promisify y almacenarla como una variable. En mi caso, eso es getChuckNorrisFact .

Luego usa esa variable como una función que puede usar como una promesa con los métodos .then () y .catch () .

Solución 2 (involucrada): Convierta la devolución de llamada en una promesa

A veces, usar las bibliotecas de solicitudes y utilidades simplemente no es posible, ya sea debido a un entorno / código base heredado o al realizar las solicitudes desde el navegador del lado del cliente, debe envolver una promesa en torno a su función de devolución de llamada.

Tomemos el ejemplo de Chuck Norris anterior y conviértalo en una promesa.

var request = require('request'); let url = "//api.chucknorris.io/jokes/random"; // A function that returns a promise to resolve into the data //fetched from the API or an error let getChuckNorrisFact = (url) => { return new Promise( (resolve, reject) => { request.get(url, function(error, response, data){ if (error) reject(error); let content = JSON.parse(data); let fact = content.value; resolve(fact); }) } ); }; getChuckNorrisFact(url).then( fact => console.log(fact) // actually outputs a string ).catch( error => console.(error) );

En el código anterior, coloco la requestfunción basada en devolución de llamada dentro de un contenedor Promise Promise( (resolve, reject) => { //callback function}). Este contenedor nos permite llamar a la getChuckNorrisFactfunción como una promesa con los métodos .then()y .catch(). Cuando el getChuckNorrisFactse llama, se ejecuta la solicitud a la API y la espera ya sea para un resolve()o una reject()sentencia que debe ejecutarse. En la función de devolución de llamada, simplemente pasa los datos recuperados a los métodos de resolución o rechazo.

Once the data (in this case, an awesome Chuck Norris fact) is fetched and passed to the resolver, the getChuckNorrisFact executes the then() method. This will return the result that you can use inside a function inside the then()to do your desired logic — in this case displaying it to the console.

You can read more about it in the MDN Web Docs.