En este artículo vamos a hablar sobre promises. Si lo desea, puede consultar las otras publicaciones de esta serie en estos enlaces:
- JavaScript Asíncrono: La Guía Completa - Parte 1 - ¿Qué es un código asíncrono?
- JavaScript Asíncrono: La Guía Completa - Parte 2 - Callbacks
- JavaScript Asíncrono: La Guía Completa - Parte 3 - Entrenando Callbacks
En los últimos artículos aprendimos sobre callbacks y vimos cómo estas funciones nos ayudan en solicitudes asíncronas. Sin embargo, en junio de 2015, llegó a nosotros la nueva versión de EcmaScript, la versión ES2015, o ES6. Una de las novedades que esta versión trajo fue el concepto de promises. Ellas llegaron para facilitar el paso de funciones de callback y también para ayudarnos a trabajar con códigos asíncronos.
¿Qué son promises?
“Promise” significa promesa. Entonces, las promises representan una promesa: intentarán ejecutar alguna función para nosotros y nos dejarán informados si lograron o no cumplir su misión. En otras palabras, una promise es un objeto que representa la falla o el éxito de un código.
Para entender mejor, piensa en esta ilustración: cuando vamos a una entrevista de trabajo, tenemos el objetivo obvio de conseguir ese trabajo. Después de la entrevista, generalmente el representante de la empresa donde fuimos nos dice la siguiente frase: “Si su perfil nos agrada, nos pondremos en contacto con usted.” Eso es una promesa, y esperamos que se convierta en realidad. Sin embargo, la llamada de esta empresa confirmando nuestra contratación es algo que está más allá de nuestro control. Puede que funcione, pero puede que no, no podemos controlar eso. Lo que podemos hacer es prepararnos para la respuesta, sea cual sea. Este es el concepto de una promise. Informamos una función a ella. Puede que funcione o no, no sabemos lo que va a suceder. Lo que podemos hacer es prepararnos para un éxito o para una falla en la ejecución de la función. Esto es algo sensacional, pues nos da un mayor control sobre nuestro código, incluso en la manipulación de errores, algo que las funciones de callback no nos ofrecen por defecto.
La sintaxis básica de una promise
Para usar una promise, es necesario crear una nueva instancia del objeto Promise. Este objeto esperará una función como argumento, que a su vez esperará dos parámetros: 1) resolve, 2) reject. Ambos son funciones:
resolve(). Es la función que confirmará el éxito de la operación si todo sale bien en la promesa.
reject(). Es la función que confirmará la “falla” de la operación si algo sale mal en la promesa. Esto puede significar un error o simplemente una excepción, algo que se aleje de lo que esperamos para nuestra función.
Solo un detalle: estos nombres nos ayudan a entender el objetivo de cada función. Sin embargo, como están representadas por variables, podemos darles el nombre que deseemos.
Veamos a continuación la creación de una promise:
function addNumbers(numberA, numberB) {
return new Promise((resolve, reject) => {
let result = numberA + numberB;
if (result > 0) {
resolve(result);
} else {
reject('El resultado es inválido');
}
});
}
addNumbers(2, 5);
Explicando: hemos creado una función que sumará dos números. Esta función devuelve una promesa, con los parámetros que mencionamos arriba: resolve y reject. Observa que usamos funciones de flecha en este ejemplo. Dentro de la promesa, hacemos una verificación: si el resultado de la cuenta es mayor que cero, es decir, si hay un resultado, ejecutaremos el resolve(), informando que todo ocurrió bien. Nota que también informaremos cuál fue el resultado de hecho. Sin embargo, si no hay resultado, rechazaremos la promesa con el método reject(), dejando claro que ocurrió algún error o excepción, a través de una cadena. Esto nos enseña otra cosa: podemos informar como parámetro para estas funciones cualquier tipo de dato: una cadena, una variable, un número o incluso otra promesa.
Además de los métodos resolve() y reject(), las promesas también tienen dos métodos muy interesantes: 1) Promise.all() y 2) Promise.race()
Promise.all(). Espera como parámetro una serie de promesas y devolverá un éxito (resolve) cuando todas ellas se ejecuten correctamente, o devolverá una falla (reject) si alguna promesa no se ejecuta completamente. Es un método usado cuando necesitamos ejecutar varias promesas al mismo tiempo.
Promise.race(). Espera como parámetro una serie de promesas y devolverá un éxito (resolve) tan pronto como la primera promesa se ejecute correctamente, o devolverá una falla (reject) cuando una promesa no se ejecute completamente.
Nos damos cuenta de que es bastante simple crear una promesa. Pero, ¿cómo vamos a manejar lo que devuelva?
“Callback” en las Promises
Las promises nos ofrecen dos métodos que podemos usar para manipular los posibles resultados: 1) then y 2) catch
then(). Este método recibe lo que nuestro resolve() informe. Se usa para manipular la respuesta de éxito de la promesa.
catch(). Este método recibe lo que nuestro reject() informe. Se usa para manipular la respuesta de error o excepción de la promesa.
Estos métodos se definirán en el lugar donde se llamó a la promesa. Veamos un ejemplo a continuación:
addNumbers(2, 5)
.then(result => {
console.log(`El resultado es ${result}`);
})
.catch(error => {
console.error(`Ocurrió un error: ${error}`);
});
Explicando: Los métodos se ejecutaron debajo de la llamada a addNumbers(). Algo muy interesante que podemos notar es que es posible romper líneas al llamar a estos métodos, lo que hace que el código sea más fácil de leer. Además, observa que la información que devolvimos en la creación de la promesa se identificó en el código anterior como dos variables: result y error. Nuevamente, como son variables, podemos sentirnos libres para elegir sus nombres.
Y ahora podemos entender un detalle muy importante: como estos dos métodos se ejecutan después de la ejecución de una función principal, que es la función contenida en nuestra promise, podemos decir que then() y catch() se ajustan al concepto de funciones de callback. Sensacional, ¿no crees? Entonces podemos ver que las promises son una forma alternativa de crear funciones de callback. Hasta se puede afirmar que es mejor usar las promises. ¿Por qué decimos eso?
Ventajas de las Promises
Una de las principales ventajas de usar promises en lugar de callbacks es lo que llamamos encadenamiento. Esto representa llamar a varias funciones en secuencia, una tras otra. Cuando hacemos esto con callbacks, nuestro código puede volverse un tanto “desordenado”, difícil de entender y mantener. Veamos este ejemplo: vamos a crear tres funciones que mostrarán información en la consola, usando la sintaxis “clásica” de funciones de callback. Las funciones son las siguientes:
function writeHello(callback) {
console.log('Hola');
callback();
}
function writeGoodBye(callback2) {
console.log('Adiós');
callback2();
}
function writeSeeYouLater(callback3) {
console.log('Hasta luego');
callback3();
}
Cada una de ellas llamará su respectivo callback. Vamos a realizar la llamada a ellas:
writeHello(function(){
writeGoodBye(function() {
writeSeeYouLater(function() {
console.log('Fin del código')
})
})
})
Este código representa un encadenamiento. Llamamos una función tras otra. Pero hay una cuestión: ¿el código no quedó un tanto extraño? ¿Parece una pirámide de códigos, verdad? Si esta estructura crece un poco más, se volverá difícil darle el mantenimiento necesario a estas funciones. Veamos ahora cómo quedaría la creación de estas mismas funciones con promises:
function writeHello() {
return new Promise((s, f) => {
console.log('Hola');
s();
});
}
function writeGoodBye() {
return new Promise((s2, f) => {
console.log('Adiós');
s2();
});
}
function writeSeeYouLater() {
return new Promise((s3, f) => {
console.log('Hasta luego');
s3();
});
}
Observa que ahora llamamos al resolve con la letra “s”, que representa el éxito de la ejecución, pero podría ser cualquier otro identificador.
Veamos a continuación cómo quedaría ahora la llamada a estas funciones:
writeHello()
.then(writeGoodBye())
.then(writeSeeYouLater())
.then(() => {
console.log('Fin del código');
})
Hemos convertido nuestro código a un encadenamiento de promesas, que seguirá funcionando sin problemas. Sin embargo, nuestro código ahora es mucho más intuitivo, más simple y más fácil de entender. Puede que tengamos que escribir un poco más, pero el código está mucho más organizado.
Así concluimos que las promises son una evolución de los callbacks y facilitan mucho la llamada a códigos asíncronos. ¿Qué opinas de poner el concepto de promises en práctica en el proyecto de Hcode Games, que estamos desarrollando juntos? ¡Vamos allá!
Necesitamos recordar dónde estábamos usando callbacks en el código: estaba en el archivo requests.js. El será alterado primero. Las funciones getGameByName() y getRelatedGamesByName() esperaban una variable llamada callback; eso ya no será necesario.
Ahora ambas funciones devolverán promesas, con las variables resolve y reject como parámetro. Además, podemos adaptar la forma de realizar la solicitud. Estábamos usando el objeto XMLHttpRequest. Sin embargo, podemos utilizar una nueva herramienta que JavaScript nos ofrece: la Fetch API. Es mucho más sencilla de usar para realizar solicitudes a un servidor y se le está llamando “el nuevo AJAX”.
Para usar este recurso, basta con llamar a la función fetch(). Espera como parámetro la URL de solicitud; y lo mejor: ¿puedes imaginar lo que nos devuelve? Una promesa :). Veamos a continuación cómo se verá nuestra función getGameByName():
function getGameByName(name) {
return new Promise((resolve, reject) => {
fetch(`https://api.rawg.io/api/games?search=${name}`)
.then(res => res.json())
.then(data => {
resolve(data);
})
.catch(err => {
reject(err);
})
});
}
Explicando: dentro de la promesa que creamos llamamos a la función fetch(). Dado que esta función nos devuelve una promesa, utilizamos los métodos .then() y .catch().
Recordando que hablamos sobre Promises y Fetch API también en nuestro Curso Completo de JavaScript.
Volviendo al código. Observa que creamos dos métodos .then(). ¿Por qué? El primero convertirá la respuesta del servidor a JSON; este código es correspondiente a ejecutar la función JSON.parse(), como estaba en el código anterior, pero ahora es mucho más simple. Así que podemos llamar a más de un método .then() en la misma promesa, si lo deseamos.
Otro detalle importante es que ahora el mensaje de error o los resultados en JSON serán devueltos a través de reject() o resolve(). Sigamos el mismo patrón para la función getRelatedGamesByName():
function getRelatedGamesByName(name) {
return new Promise((resolve, reject) => {
fetch(`https://api.rawg.io/api/games/${name}/suggested`)
.then(res => res.json())
.then(data => {
resolve(data);
})
.catch(err => {
reject(err);
})
});
}
¡Perfecto! Ahora podemos ir a nuestro archivo HTML.
Llamamos a estas funciones solo en la función initGames() y será ella la que vamos a alterar. Ahora solo informaremos el nombre del juego a las funciones y en lugar de añadir el código de callback en una función anónima, usaremos el método .then(). El código final quedará así:
function initGames(gamename) {
setGameLoad(gamesListPrincipal);
gamesListRelationed.innerHTML = '';
getGameByName(gamename).then(games => {
gamesListPrincipal.innerHTML = '';
games.results.forEach(game => {
let divGame = setGameHTML(game);
divGame.addEventListener('click', e => {
setGameLoad(gamesListRelationed);
let gameTag = e.currentTarget;
let gamename = gameTag.dataset.gamename;
getRelatedGamesByName(gamename).then(gamesRelationed => {
gamesListRelationed.innerHTML = '';
gamesRelationed.results.forEach(game => {
let divGameRelationed = setGameHTML(game);
gamesListRelationed.append(divGameRelationed);
});
});
});
gamesListPrincipal.append(divGame);
});
});
}
¡Alteraciones finalizadas! Ahora, como decía el título del primer Karate Kid: “Ahora es la hora de la verdad”. Nuestro proyecto necesita seguir funcionando. Vamos a probarlo:
Genial, sigue funcionando igual que antes, pero ahora nuestro proyecto está modelado en el concepto de Promises.
En este artículo aprendimos qué son las Promises en JavaScript, y prometo que te ayudarán mucho en el desarrollo (perdona el juego de palabras, no pude evitarlo). Puedes encontrar todos los códigos desarrollados en este artículo en este enlace: https://github.com/hcodebr/javascript-assincrono-blog
Si te gustó el contenido de este artículo, te pedimos que lo compartas, para que otras personas también puedan acceder a este material, ¡muchas gracias!
Las promesas son realmente increíbles, muy fáciles de implementar. Sin embargo, JavaScript tiene aún otro recurso, introducido en su última versión, que también ayuda mucho en códigos asíncronos, facilitando aún más el uso de las promesas. Estamos hablando de las funciones asíncronas, o async/await. Hablaremos de ellas con más detalles en el próximo artículo: JavaScript Asíncrono: La Guía Completa - Parte 5 - Entendiendo Async/Await, hasta entonces :)