Promesas de JavaScript: un tutorial con ejemplos
Publicado: 2022-03-11Las promesas son un tema candente en los círculos de desarrollo de JavaScript, y definitivamente deberías familiarizarte con ellas. No son fáciles de entender; puede tomar algunos tutoriales, ejemplos y una buena cantidad de práctica para comprenderlos.
Mi objetivo con este tutorial es ayudarlo a comprender las promesas de JavaScript y alentarlo a que practique su uso más. Estaré explicando qué son las promesas, qué problemas resuelven y cómo funcionan. Cada paso, descrito en este artículo, va acompañado de un ejemplo de código jsbin
para ayudarlo a trabajar y para usarlo como base para una mayor exploración.
¿Qué es una promesa de JavaScript?
Una promesa es un método que eventualmente produce un valor. Puede considerarse como la contraparte asíncrona de una función getter. Su esencia se puede explicar como:
promise.then(function(value) { // Do something with the 'value' });
Las promesas pueden reemplazar el uso asíncrono de las devoluciones de llamadas y brindan varios beneficios sobre ellas. Comienzan a ganar terreno a medida que más y más bibliotecas y marcos los adoptan como su forma principal de manejar la asincronía. Ember.js es un gran ejemplo de dicho marco.
Hay varias bibliotecas que implementan la especificación Promises/A+. Aprenderemos el vocabulario básico y trabajaremos con algunos ejemplos de promesas de JavaScript para presentar los conceptos detrás de ellos de una manera práctica. Usaré una de las bibliotecas de implementación más populares, rsvp.js, en los ejemplos de código.
¡Prepárate, tiraremos muchos dados!
Obtener la biblioteca rsvp.js
Las promesas, y por lo tanto rsvp.js, se pueden usar tanto en el servidor como en el lado del cliente. Para instalarlo para nodejs , vaya a la carpeta de su proyecto y escriba:
npm install --save rsvp
Si trabaja en el front-end y usa Bower, es solo un
bower install -S rsvp
fuera.
Si solo desea ingresar directamente al juego, puede incluirlo a través de una etiqueta de script simple (y con jsbin
, puede agregarlo a través del menú desplegable "Agregar biblioteca"):
<script src="//cdn.jsdelivr.net/rsvp/3.0.6/rsvp.js"></script>
¿Qué propiedades tiene una promesa?
Una promesa puede estar en uno de tres estados: pendiente , cumplida o rechazada . Cuando se crea, la promesa está en estado pendiente. Desde aquí, puede pasar al estado cumplido o rechazado. Llamamos a esta transición la resolución de la promesa . El estado resuelto de una promesa es su estado final, por lo que una vez cumplida o rechazada, se queda allí.
La forma de crear una promesa en rsvp.js es a través de lo que se llama un constructor revelador. Este tipo de constructor toma un solo parámetro de función e inmediatamente lo llama con dos argumentos, fulfill
y reject
, que pueden hacer la transición de la promesa al estado fulfilled
o rejected
:
var promise = new RSVP.Promise(function(fulfill, reject) { (...) });
Este patrón de promesas de JavaScript se denomina constructor revelador porque el argumento de función única revela sus capacidades a la función constructora, pero garantiza que los consumidores de la promesa no puedan manipular su estado.
Los consumidores de la promesa pueden reaccionar a sus cambios de estado agregando su controlador a través del método then
. Se necesita una función de controlador de cumplimiento y de rechazo, las cuales pueden faltar.
promise.then(onFulfilled, onRejected);
Según el resultado del proceso de resolución de la promesa, el onFulfilled
o onRejected
se llama de forma asíncrona .
Veamos un ejemplo que muestra en qué orden se ejecutan las cosas:
function dieToss() { return Math.floor(Math.random() * 6) + 1; } console.log('1'); var promise = new RSVP.Promise(function(fulfill, reject) { var n = dieToss(); if (n === 6) { fulfill(n); } else { reject(n); } console.log('2'); }); promise.then(function(toss) { console.log('Yay, threw a ' + toss + '.'); }, function(toss) { console.log('Oh, noes, threw a ' + toss + '.'); }); console.log('3');
Este fragmento imprime una salida similar a la siguiente:
1 2 3 Oh, noes, threw a 4.
O, si tenemos suerte, vemos:
1 2 3 Yay, threw a 6.
Este tutorial de promesas demuestra dos cosas.
En primer lugar, que los controladores que adjuntamos a la promesa fueron efectivamente llamados después de que se ejecutara el resto del código, de forma asíncrona.
Segundo, que el manejador de cumplimiento se llamara solo cuando se cumplió la promesa, con el valor con el que se resolvió (en nuestro caso, el resultado del lanzamiento de dados). Lo mismo es válido para el controlador de rechazo.
Encadenando promesas y goteando
La especificación requiere que la función then
(los controladores) también devuelva una promesa, lo que permite encadenar promesas, lo que da como resultado un código que parece casi sincrónico:
signupPayingUser .then(displayHoorayMessage) .then(queueWelcomeEmail) .then(queueHandwrittenPostcard) .then(redirectToThankYouPage)
Aquí, signupPayingUser
devuelve una promesa, y cada función de la cadena de promesa se llama con el valor de retorno del controlador anterior una vez que se ha completado. A todos los efectos prácticos, esto serializa las llamadas sin bloquear el hilo de ejecución principal.
Para ver cómo se resuelve cada promesa con el valor de retorno del elemento anterior en la cadena, volvemos a tirar los dados. Queremos tirar los dados un máximo de tres veces, o hasta que salgan las seis primeras jsbin:
function dieToss() { return Math.floor(Math.random() * 6) + 1; } function tossASix() { return new RSVP.Promise(function(fulfill, reject) { var n = Math.floor(Math.random() * 6) + 1; if (n === 6) { fulfill(n); } else { reject(n); } }); } function logAndTossAgain(toss) { console.log("Tossed a " + toss + ", need to try again."); return tossASix(); } function logSuccess(toss) { console.log("Yay, managed to toss a " + toss + "."); } function logFailure(toss) { console.log("Tossed a " + toss + ". Too bad, couldn't roll a six"); } tossASix() .then(null, logAndTossAgain) //Roll first time .then(null, logAndTossAgain) //Roll second time .then(logSuccess, logFailure); //Roll third and last time
Cuando ejecute este código de ejemplo de promesas, verá algo como esto en la consola:
Tossed a 2, need to try again. Tossed a 1, need to try again. Tossed a 4. Too bad, couldn't roll a six.
La promesa devuelta por tossASix
se rechaza cuando el lanzamiento no es un seis, por lo que se llama al controlador de rechazo con el lanzamiento real. logAndTossAgain
imprime ese resultado en la consola y devuelve una promesa que representa otro lanzamiento de dados. Ese lanzamiento, a su vez, también es rechazado y cerrado por el siguiente logAndTossAgain
.
A veces, sin embargo, tienes suerte* y consigues sacar un seis:
Tossed a 4, need to try again. Yay, managed to toss a 6.
* No tienes que tener tanta suerte. Hay un 42 % de posibilidades de sacar al menos un seis si lanzas tres dados.

Ese ejemplo también nos enseña algo más. ¿Ves cómo no se hicieron más lanzamientos después de la primera tirada exitosa de un seis? Tenga en cuenta que todos los controladores de cumplimiento (los primeros argumentos en las llamadas a then
) en la cadena son null
, excepto el último, logSuccess
. La especificación requiere que si un controlador (cumplimiento o rechazo) no es una función, la promesa devuelta debe resolverse (cumplirse o rechazarse) con el mismo valor. En el ejemplo de promesas anterior, el controlador de cumplimiento, null
, no es una función y el valor de la promesa se cumplió con un 6. Por lo tanto, la promesa devuelta por la llamada then
(la siguiente en la cadena) también se cumplirá. con 6 como su valor.
Esto se repite hasta que un controlador de cumplimiento real (uno que es una función) esté presente, por lo que el cumplimiento se filtra hasta que se maneja. En nuestro caso, esto sucede al final de la cadena, donde alegremente se desconecta de la consola.
Manejo de errores
La especificación Promises/A+ exige que si se rechaza una promesa o se arroja un error en un controlador de rechazo, debe ser manejado por un controlador de rechazo que esté "aguas abajo" de la fuente.
Aprovechar la siguiente técnica de goteo proporciona una forma limpia de manejar los errores:
signupPayingUser .then(displayHoorayMessage) .then(queueWelcomeEmail) .then(queueHandwrittenPostcard) .then(redirectToThankYouPage) .then(null, displayAndSendErrorReport)
Debido a que un controlador de rechazo solo se agrega al final de la cadena, si cualquier controlador de cumplimiento en la cadena es rechazado o arroja un error, se filtra hasta que se topa con displayAndSendErrorReport
.
Volvamos a nuestros amados dados y veamos eso en acción. Supongamos que solo queremos lanzar los dados de forma asincrónica e imprimir los resultados:
var tossTable = { 1: 'one', 2: 'two', 3: 'three', 4: 'four', 5: 'five', 6: 'six' }; function toss() { return new RSVP.Promise(function(fulfill, reject) { var n = Math.floor(Math.random() * 6) + 1; fulfill(n); }); } function logAndTossAgain(toss) { var tossWord = tossTable[toss]; console.log("Tossed a " + tossWord.toUppercase() + "."); } toss() .then(logAndTossAgain) .then(logAndTossAgain) .then(logAndTossAgain);
Cuando ejecutas esto, no pasa nada. No se imprime nada en la consola y aparentemente no se arrojan errores.
En realidad, se lanza un error, simplemente no lo vemos ya que no hay controladores de rechazo en la cadena. Dado que el código en los controladores se ejecuta de forma asíncrona, con una pila nueva, ni siquiera se desconecta de la consola. Arreglemos esto:
function logAndTossAgain(toss) { var tossWord = tossTable[toss]; console.log("Tossed a " + tossWord.toUpperCase() + "."); } function logErrorMessage(error) { console.log("Oops: " + error.message); } toss() .then(logAndTossAgain) .then(logAndTossAgain) .then(logAndTossAgain) .then(null, logErrorMessage);
Ejecutar el código anterior muestra el error ahora:
"Tossed a TWO." "Oops: Cannot read property 'toUpperCase' of undefined"
Olvidamos devolver algo de logAndTossAgain
y la segunda promesa se cumple con undefined
. El siguiente controlador de cumplimiento luego explota tratando de llamar a toUpperCase
en eso. Esa es otra cosa importante para recordar: siempre devuelva algo de los controladores, o prepárese en los controladores posteriores para que no pase nada.
Construyendo más alto
Ahora hemos visto los conceptos básicos de las promesas de JavaScript en el código de ejemplo de este tutorial. Un gran beneficio de usarlos es que se pueden componer de manera simple para producir promesas "compuestas" con el comportamiento que nos gustaría. La biblioteca rsvp.js
proporciona un puñado de ellos, y siempre puede crear uno propio utilizando los primitivos y estos de nivel superior.
Para el último ejemplo, el más complejo, viajamos al mundo de los juegos de rol de AD&D y tiramos dados para obtener puntajes de personajes. Dichas puntuaciones se obtienen tirando tres dados por cada habilidad del personaje.
Permítanme pegar el código aquí primero y luego explicar qué hay de nuevo:
function toss() { var n = Math.floor(Math.random() * 6) + 1; return new RSVP.resolve(n); // [1] } function threeDice() { var tosses = []; function add(x, y) { return x + y; } for (var i=0; i<3; i++) { tosses.push(toss()); } return RSVP.all(tosses).then(function(results) { // [2] return results.reduce(add); // [3] }); } function logResults(result) { console.log("Rolled " + result + " with three dice."); } function logErrorMessage(error) { console.log("Oops: " + error.message); } threeDice() .then(logResults) .then(null, logErrorMessage);
Estamos familiarizados con el toss
del último ejemplo de código. Simplemente crea una promesa que siempre se cumple con el resultado de tirar un dado. RSVP.resolve
, un método conveniente que crea tal promesa con menos ceremonia (ver [1] en el código anterior).
En threeDice
, creé 3 promesas, cada una de las cuales representa un lanzamiento de dados, y finalmente las combiné con RSVP.all
. RSVP.all
toma una matriz de promesas y se resuelve con una matriz de sus valores resueltos, uno para cada promesa constituyente, manteniendo su orden. Eso significa que tenemos el resultado de los lanzamientos en results
(ver [2] en el código anterior), y devolvemos una promesa que se cumple con su suma (ver [3] en el código anterior).
Al resolver la promesa resultante, se registra el número total:
"Rolled 11 with three dice"
Usar promesas para resolver problemas reales
Las promesas de JavaScript se utilizan para resolver problemas en aplicaciones que son mucho más complejas que los lanzamientos de dados asincrónicos sin una buena razón .
Si sustituye lanzar tres dados con el envío de tres solicitudes ajax a puntos finales separados y continuar cuando todos hayan regresado con éxito (o si alguno de ellos falló), ya tiene una aplicación útil de promesas y RSVP.all
.
Las promesas, cuando se usan correctamente, producen un código fácil de leer sobre el que es más fácil razonar y, por lo tanto, más fácil de depurar que las devoluciones de llamada. No es necesario establecer convenciones relacionadas, por ejemplo, con el manejo de errores, ya que ya forman parte de la especificación.
Apenas arañamos la superficie de lo que pueden hacer las promesas en este tutorial de JavaScript. Las bibliotecas de Promise proporcionan una buena docena de métodos y constructores de bajo nivel que están a su disposición. Domínelos, y el cielo es el límite de lo que puede hacer con ellos.
Sobre el Autor
Balint Erdi fue un gran fanático de los juegos de rol y AD&D hace mucho tiempo, y ahora es una gran promesa y fanático de Ember.js. Lo que ha sido constante es su pasión por el rock & roll. Por eso decidió escribir un libro sobre Ember.js que utiliza el rock & roll como tema de la aplicación en el libro. Regístrese aquí para saber cuándo se lanza.