Explicaciones detalladas sobre las promesas en Javascript. Qué son las promesas, cómo usarlas en Javascript. Cómo hacer funciones que devuelven promesas y cómo resolver promesas en secuencia y en paralelo.
Las promesas son herramientas de los lenguajes de programación que nos sirven para gestionar situaciones futuras en el flujo de ejecución de un programa. Aunque es un concepto que usamos en Javascript desde hace un relativamente corto espacio de tiempo, ya se viene implementando en el mundo de la programación desde la década de los 70.
Las promesas se originaron en el ámbito de la programación funcional, aunque diversos paradigmas las han incorporado, generalmente para gestionar la programación asíncrona. En resumen, nos permiten definir cómo se tratará un dato que sólo estará disponible en un futuro. Es decir, mediante las promesas podemos indicar qué se realizará con ese dato más adelante cuando haya sido devuelto por un proceso asíncrono.
Ahora con ES6 podemos beneficiarnos de las ventajas de las promesas a la hora de escribir un código más limpio y claro. De hecho son una de las novedades más destacadas de ES6.
Para abordar las Promesas en Javascript con todo el detalle que se merece hemos estructurado este artículo en los siguientes apartados de interés.
- Cómo usar promesas
- Pirámide de callbacks
- Escribir funciones que devuelven promesas
- Encadenar promesas
- Conclusión
- Encadenar promesas transmitiendo datos de unas a otras
- Desarrollar funciones que devuelven promesas
- Funciones resolve y reject
- Cómo implementar una conexión Ajax con fetch que devuelve un texto
- Promesas Javascript en secuencia o en paralelo
- Promesas en secuencia
- Encadenar más promesas en secuencia
- Ejecutar promesas en paralelo
- Conclusión
Cómo usar promesas
Creo que para comenzar a entender las promesas nos viene muy bien comenzar viendo cómo podemos gestionarlas, es decir, qué se debe hacer cuando ejecutamos funciones que nos devuelven promesas. Luego aprenderemos a crear funciones que implementan promesas y veremos que es también bastante fácil.
Por ejemplo, la función set() de Firebase, para guardar datos en la base de datos en tiempo real, devuelve una promesa cuando se realiza una operación de escritura.
Tiene sentido que se use una promesa porque, aunque Firebase es realmente rápido, siempre va a existir un espacio de tiempo entre que solicitamos realizar una escritura de un dato y que ese dato se escribe realmente en la base de datos. Además, la escritura podría dar algún tipo de problema y por tanto producirse un error de ejecución, que también deberíamos gestionar. Todas esas situaciones se pueden implementar por medio de dos métodos:
- then: usado para indicar qué hacer en caso que la promesa se haya ejecutado con éxito.
- catch: usado para indicar qué hacer en caso que durante la ejecución de la operación se ha producido un error.
Ambos métodos debemos usarlos pasándoles la función callback a ejecutar en cada una de esas posibilidades.
referenciaFirebase.set(data)
.then(function(){
console.log('el dato se ha escrito correctamente');
})
.catch(function(err) {
console.log('hemos detectado un error', err');
});
Fíjate que "referenciaFirebase.set(data)" nos devuelve una promesa. Sobre esa promesa encadenamos dos métodos, then() y catch(). Esos dos métodos son para gestionar el futuro estado de la escritura en Firebase y están encadenados a la promesa. Por si acaso no se entiende eso, podríamos leer este mismo código de esta manera.
referenciaFirebase.set(data).then(function(){
console.log('el dato se ha escrito correctamente');
}).catch(function(err) {
console.log('hemos detectado un error', err');
});
Igual así se ve mejor qué es lo que me refiero cuando digo que están encadenados. Insisto en esto para que nos demos cuenta que los then() y catch() forman parte de la misma cosa (la promesa devuelta por el método set() de Firebase). Además, porque encadenar promesas es algo bastante normal y así nos vamos familiarizando mejor con cosas que usaremos en un futuro próximo.
Datos devueltos por las promesas
Otro detalle que no debe pasar desapercibido es que la promesa puede devolver datos. Es muy normal que esto ocurra. Por ejemplo queremos recibir algo de una base de datos y cuando la promesa se ejecuta correctamente querremos que nos llege ese dato buscado. No es el caso en este método set() de Firebase, porque una operación de escritura no te devuelve nada en esta base de datos, pero insisto que es algo bastante común.
En el caso que la promesa te devuelva un dato, lo podrás recibir como parámetro en la función callback que estás adjuntando al then().
funcionQueDevuelvePromesa()
.then( function(datoProcesado){
//hacer algo con el datoProcesado
})
En el caso negativo implementado mediante el catch() siempre vamos a recibir un dato, que es el error que se ha producido al ejecutar la promesa y el causante de estar procesándose el correspondiente catch. Volviendo al ejemplo de antes, método set(), observa que el error lo hemos recibido en el parámetro de la función callback indicada en el catch().
Pirámide de callbacks
Seguro que habrás oído hablar del código spaguetti. Uno de los síntomas en Javascript de ello es lo que se conoce como pirámide de callbacks o "callback hell". Hay mucha literatura y ejemplos en Javascript sobre ello. Ocurre cuando quieres hacer una operación asíncrona, a la que le colocas un callback para continuar tu ejecución. Luego quieres encadenar una nueva operación cuando acaba la anterior y otra nueva cuando acaba ésta.
El método setTimeout() de toda la vida en Javascript nos sirve para escribir algo de código spaguetti y ver la temida pirámide de callbacks.
setTimeout(function() {
console.log('hago algo');
setTimeout(function() {
console.log('hago algo 2');
setTimeout(function() {
console.log('hago algo 3');
setTimeout(function() {
console.log('hago algo 4');
}, 1000)
}, 1000)
}, 1000)
}, 1000);
En resumen lo que hacemos es encadenar una serie de tareas, para realizarlas secuencialmente, una cuando acaba la otra. Funciona, pero ese código es un infierno para mantener, pues tiene difícil lectura y cuesta meterle mano para implementar nuevas funcionalidades. Las promesas nos pueden ayudar a mejorarlo, pero primero vamos a tener que aprender a implementarlas nosotros mismos.
Escribir funciones que devuelven promesas
Ahora viene otra parte interesante, en la que aprendemos a crear nuestras propias funciones que devuelven promesas. Esto se consigue mediante la creación de un nuevo objeto "Promise", como veremos a continuación. Pero antes de ponernos con ello debes tener bien claro el objetivo de una promesa: "hacer algo que dura un tiempo y luego tener la capacidad de informar sobre posibles casos de éxito y de fracaso"
Ahora verás el código y aunque pueda parecer confuso al principio, la experiencia usando promesas te lo irá clarificando naturalmente. Ten en cuenta que para crear un objeto "Promise" voy a tener que entregarle una función, la encargada de realizar ese procesamiento que va a tardar algo de tiempo. En esa función debo ser capaz de procesar casos de éxito y fracaso y para ello recibo como parámetros dos funciones:
- La función "resolve": la ejecutamos cuando queremos finalizar la promesa con éxito.
- La función "reject": la ejecutamos cuando queremos finalizar una promesa informando de un caso de fracaso.
function hacerAlgoPromesa() {
return new Promise( function(resolve, reject){
console.log('hacer algo que ocupa un tiempo...');
setTimeout(resolve, 1000);
})
}
Como puedes ver, nuestra función hacerAlgoPromesa() devolverá siempre una promesa (return new Promise). Se encarga de hacer alguna cosa, y luego ejecutará el método resolve (1 segundo después, gracias al serTimeout).
Esa misma función algunos programadores la preferirían ver escrita de este otro modo.
function hacerAlgoPromesa(tarea) {
function haciendoalgo(resolve, reject) {
console.log('hacer algo que ocupa un tiempo...');
setTimeout(resolve, 1000);
}
return new Promise( haciendoalgo );
}
Es exactamente lo mismo que teníamos antes, solo que se ha ordenado el código de otra manera. Usa la alternativa que veas más clara.
Ahora vamos a ver cómo ejecutar esta función que nos devuelve una promesa, aunque si entendiste el principio del artículo ya lo tendrás bastante claro.
hacerAlgoPromesa()
.then( function() {
console.log('la promesa terminó.');
})
Un poco más adelante en este mismo artículo profundizaremos sobre este asunto y mostrarmos nuevos ejemplos sobre cómo crear tus propias funciones que devuelven promesas y veremos cómo tratar los casos negativos y rechazar promesas con reject().
Encadenar promesas
Como colofón a esta introducción a las promesas de ES6 queremos ver cómo nos facilitan la vida, creando un código mucho más limpio y entendible que la famosa pirámide de callbacks que hemos visto en un punto anterior. Si no estás familiarizado con el tema estoy seguro que te sorprenderás. Para que sea así, vamos directamente con el código fuente:
Imagina que quieres hacer algo y repetirlo por cuatro veces, ejecutando la función hacerAlgoPromesa() repetidas veces, de manera secuencial, una después de la otra.
hacerAlgoPromesa()
.then( hacerAlgoPromesa )
.then( hacerAlgoPromesa )
.then( hacerAlgoPromesa )
Eso, comparado con el spaguetti code de antes, tiene su diferencia ¿no? y es básicamente lo mismo, ejecutar una acción 4 veces con un retardo entre ellas.
Encadenar promesas transmitiendo datos de unas a otras
A partir de aquí queda todavía por abordar diferentes puntos interesantes y útiles, como controlar posibles casos de error e informar de ellos en nuestras promesas, o poder ejecutar varias promesas en paralelo, en vez de secuencialmente. Todo eso lo iremos tratando, aunque espero que con este artículo se te abra un poco de luz y que puedas apreciar algunas de las ventajas de usar promesas ES6.
Si quieres investigar algo más sobre encadenar promesas, piensa que a veces a las funciones que devuelven promesas les tienes que pasar parámetros. ¿Cómo escribirías el chaining de promises? Para ser más claros, echa un vistazo a esta promesa.
function hacerAlgoPromesa2(tarea) {
function haciendoalgo(resolve, reject) {
console.log('Hacer ' + tarea + ' que ocupa un tiempo...');
setTimeout(resolve, 1000);
}
return new Promise( haciendoalgo );
}
Es casi casi lo mismo que teníamos antes, solo que ahora le podemos pasar la tarea que quieres realizar. Lo que queremos es encadenar cuatro tareas diferentes, para ejecutar en secuencial, igual que antes. Pero tienes que pasarles parámetros distintos.
La solución la encuentras en el siguiente pedazo de código.
hacerAlgoPromesa('documentar un tema')
.then(function() {
return hacerAlgoPromesa('escribir el artículo')
})
.then(function() {
return hacerAlgoPromesa('publicar en desarrolloweb.com')
})
.then(function() {
return hacerAlgoPromesa('recibir vuestro apoyo cuando compartís en vuestras redes sociales')
})
Échale un vistazo y trata de entenderlo. La clave es que para encadenar promesas la función a ejecutar como callback debe devolver también una nueva promesa.
Nos hemos quitado de en medio la pirámide de callbacks, ero tampoco creas que sería el mejor código para resolver este problema. Ya que estamos en un Manual de ES6, no queremos perder la oportunidad de mostrar un ejemplo del azúcar sintáctico que nos ofrecen las Arrow Functions.
Este código sería equivalente al anterior:
hacerAlgoPromesa('documentar un tema')
.then(() => hacerAlgoPromesa('escribir el artículo'))
.then(() => hacerAlgoPromesa('publicar en desarrolloweb.com'))
.then(() => hacerAlgoPromesa('...compartís en vuestras redes sociales'))
Mucho más limpio, no?
Ahora ya conoces una de las mejoras más interesantes de la versión de ECMAScript 2015 (ES6), que es la posibilidad de gestionar el código asíncrono por medio de promesas. Este estilo de programación facilita mucho la legibilidad y el mantenimiento del código.
Hemos podido aprender lo básico de las promesas, centrándonos en cómo usr promesas y por qué son tan útiles en Javascript. De todos modos, para sacarles de verdad partido y resolver muchas de las necesidades de tus aplicaciones tendrás que aprender a crear tus propias funciones asíncronas que devuelven promesas y usar Resolve / Reject.
Desarrollar funciones que devuelven promesas
Hasta este punto hemos aprendido lo que son las promesas y cómo usar funciones que devuelven promesas con then
y catch
para organizar nuestro código. Vimos que en el lenguaje Javascript es fácil caer en lo que se conoce como "código spaguetti" y que las promesas nos ofrecen una vía excelente para organizar nuestro código evitando la pirámide de callbacks. También vimos algunos ejemplos sobre cómo crear funciones que devuelven promesas, pero nos quedamos en lo elemental.
Ahora vamos a ampliar la información en lo referente a la implementación de una función que devuelve una promesa, tratando tanto los casos positivos (éxito) como los casos negativos (fracaso). Además ahondaremos en otra información importante de lenguaje Javascript y la nueva API de acceso a recursos Ajax: fetch.
Funciones resolve y reject
Cuando implementamos una función que devuelve una promesa tenemos a nuestra disposición dos funciones que permiten devolver el control al código que invocó la promesa. Esas funciones devuelven datos cuando la promesa se ejecutó normalmente y produjo los resultados esperados y cuando la promesa produjo un error o no pudo alcanzar los resultados deseados.
El código de una función que devuelve una promesa tiene una forma inicial como esta:
function devuelvePromesa() {
return new Promise( (resolve, reject) => {
//realizamos nuestra operativa…
})
}
Como puedes ver, este código devuelve una nueva promesa. Para crear la promesa usamos "new Promise". El constructor de la promesa lo alimentamos con una función que recibe dos parámetros: resolve y reject, que son a su vez funciones que podremos usar para devolver valores tanto en el caso positivo como en el caso negativo.
Ahora veamos nuestra operativa, que podría tener una forma como esta:
function devuelvePromesa() {
return new Promise( (resolve, reject) => {
setTimeout(() => {
let todoCorrecto = true;
if (todoCorrecto) {
resolve('Todo ha ido bien');
} else {
reject('Algo ha fallado)
}
}, 2000)
})
}
En nuestro código realizaremos cualquier tipo de proceso, generalmente asíncrono, por lo que tendrá un tiempo de ejecución durante el cual se devolverá el control por medio de una función callback. En la función callback podremos saber si aquel proceso se produjo de manera correcta o no. Si fue correcto usaremos la función resolve(), mandando de vuelta como parámetro el valor que se haya conseguido como resultado. Si algo falló usaremos la función reject(), enviando el motivo del error.
Podremos usar esa función que devuelve la promesa con un código como este:
devuelvePromesa()
.then( respuesta => console.log(respuesta) )
.catch( error => console.log(error) )
Como ya supondrás, then() recibe la respuesta indicada en el resolve() y catch() el error indicado en el reject.
Cómo implementar una conexión Ajax con fetch que devuelve un texto
En el artículo de fetch vimos que es un nuevo modelo de trabajo con Ajax, pero usando promesas. Vimos que un fetch() te devuelve una respuesta del servidor, con datos sobre la solicitud HTTP, pero si lo que queríamos es acceder al texto de la respuesta, necesitábamos encadenar una segunda promesa, llamando al método text() sobre la respuesta. Esa segunda promesa nos complicó un poco el código de algo tan sencillo como es: dada una URL recibir el texto que hay en el recurso.
A continuación vamos a poner un código de alternativa, que realiza ambas promesas y directamente nos devuelve el texto de respuesta. Como es un código asíncrono, que tardará un poco en ejecutarse y después de ello debe devolver el control al script original, lo implementaremos por medio de una promesa.
function obtenerTexto(url) {
return new Promise( (resolve, reject) => {
fetch(url)
.then(response => {
if(response.ok) {
return response.text();
}
reject('No se ha podido acceder a ese recurso. Status: ' + response.status);
})
.then( texto => resolve(texto) )
.catch (err => reject(err) );
});
}
Este código usa el modelo de encadenado de promesas, ejecutando operaciones asíncronas, una cuando termina la anterior.
Comenzamos haciendo el fetch() a una URL que recibimos por parámetro. Ese fetch devuelve una promesa, que cuando se ejecuta correctamente nos entrega la respuesta del servidor.
Si la respuesta estuvo bien (response.ok es true) entonces devuelve una nueva promesa entregada por la ejecución de la función response.text(). Si la respuesta no estuvo bien, entonces rechazamos la promesa con reject(), indicando el motivo por el que estamos rechazando con un error.
A su vez el código de response.text(), que devolvía otra promesa, puede dar un caso de éxito o uno de error. El caso de éxito lo tratamos con el segundo then(), en el que aceptamos la promesa con el resolve.
Tanto para el caso de error de fetch() como para un caso de error en el método text(), como para cualquier otro error detectado (incluso un código mal escrito) realizamos el correspondiente catch(), rechazando nuestra promesa original.
Este código lo puedes usar de la siguiente manera. Verás que tenemos un único método que nos devuelve directamente el texto.
obtenerTexto('test.txt')
.then( texto => console.log(texto) )
.catch( err => console.log('ERROR', err) )
Procesamiento de múltiples Promesas Javascript
Ya sabes usar promesas e implementar funciones que devuelven promesas. Es un gran paso para mejorar el código asíncrono de las aplicaciones Javascript. Sin embargo todavía hay más que deberías saber para sacarles todo el partido.
A continuación vamos a ir un paso más allá, viendo ejemplos de trabajo con promesas un poco más complejos, combinando varias ejecuciones. Y es que muchas veces las promesas no vienen por separado, sino que tienes que ejecutar varias promesas una detrás de otra, porque unas dependan entre sí, o ejecutarlas todas a la vez y recibir una señal cuando se ha completado el conjunto entero.
Promesas en secuencia
En muchos casos unas promesas dependen de otras, es decir, tienes que esperar que una promesa haya terminado y, con su resultado, ejecutar otra. En estos casos decimos que las promesas deben ejecutarse en secuencia.
El ejemplo más típico de realización de una promesa en secuencia es el que hacemos para resolver una solicitud Ajax mediante Fetch, dado que para obtener el JSON de un servicio web necesitamos encadenar dos promesas. La primera para recibir una respuesta del servidor y la segunda para procesar esa respuesta y quedarnos con el cuerpo en un objeto.
La manera de ejecutar promesas en secuencia es encadenar varios "then", como puedes ver aquí.
fetch("https://jsonplaceholder.typicode.com/todos/")
.then( response => response.json() )
.then( json => console.log(json) )
La función fetch() devuelve una promesa, que se resuelve cuando se recibe la respuesta del servidor. El primer "then" recibe la respuesta del servidor. Puedes examinar la respuesta para ver si es válida o verificar cualquier otra cosa. Sin embargo aquí solamente ejecutamos response.json() para obtener el objeto de respuesta. El método response.json() devuelve otra promesa que se encadena. El segundo "then" se ejecuta cuando se resuelve la segunda promesa y en él recibimos ya el json procesado y podemos hacer cualquier cosa con él.
Nota: Si te interesa explorar más esta modalidad de trabajo con Ajax te recomendamos el artículo sobre fetch.
Date cuenta que, siempre que devolvamos una promesa nueva, podremos enganchar los "then", que esperarán a que la promesa se resuelva para continuar.
Encadenar más promesas en secuencia
El ejemplo se puede complicar todo lo que queramos. Ahora vamos a acceder a dos servicios web y esperar 5 segundos entre uno y otro.
Para el acceso a los servicios web usaremos fetch, igual que antes. Entre medias haremos una pausa para poder espaciar estos accesos. Para poder ver todo esto con distintas promesas hemos creado una función que devuelve una promesa, que simplemente se resuelve después de una espera.
const esperar = tiempo => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('ok');
}, tiempo);
})
}
Ahora veamos el código que ejecuta toda la secuencia de promesas de la que hemos hablado. Las dos promesas del acceso al primer servicio web, la espera de 5 segundos y finalmente las dos promesas de acceso al segundo servicio web.
fetch("https://jsonplaceholder.typicode.com/todos/")
.then( response => response.json())
.then( json => console.log(json))
.then(() => esperar(5000))
.then( res => console.log(res))
.then(() => fetch("https://pokeapi.co/api/v2/pokemon/1"))
.then( response => response.json())
.then( json => console.log(json));
¿Qué te parece? normalmente no tienes tantas promesas que ejecutar en secuencia, pero si fuera el caso puedes hacer un código bastante compacto y evitar lo que se llama la pirámide de callbacks.
Generalmente, cada una de las funciones de los "then" devuelven nuevas promesas que se irán ejecutando cuando acabe la anterior. Sin embargo, esto no tiene por qué ser siempre así. Por ejemplo, por en medio hemos colocado una función de un "then" que no cumple esta norma. ¿La encuentras?
Seguramente la hayas visto, pero si no, es la siguiente:
.then( res => console.log(res))
Esa función callback del "then" no devuelve una promesa. De hecho no devuelve ningún valor. Esa situación no es un problema en realidad, simplemente el próximo "then" se ejecutará inmediatamente después del anterior, sin tener que esperar nada, puesto que no se ha devuelto una promesa que tarde en resolverse.
Ejecutar promesas en paralelo
No siempre se necesitan ejecutar todas las promesas una detrás de otra. Muchas veces no son dependientes entre sí y podemos ejecutarlas a la vez, para ahorrar tiempo.
Podríamos simplemente colocar el código de todas las promesas suelto e independiente entre sí. Algo como:
esperar(5000).then((res) => console.log("Uno"));
esperar(1000).then((res) => console.log("Dos"));
esperar(3000).then((res) => console.log("Tres"));
console.log("Fin!");
¿En qué orden piensas que se mostrarán los distintos mensajes a la consola?
Seguramente lo has adivinado…. será: "Fin! Dos Tres Uno".
Pero a veces tienes que esperar a que se ejecuten todas las promesas para luego hacer alguna acción con todos los valores de respuesta. Por ejemplo, dado el código anterior nos gustaría que el mensaje "Fin!" fuera el último que se mostrase.
Quizás en este ejemplo está claro porque sabemos que la promesa que más tiempo va a tardar en ejecutarse es la primera, que espera 5 segundos. Podríamos escribir el mensaje "Fin!" cuando ella termine. Sin embargo, en la mayoría de las ocasiones no sabemos qué promesa va a terminar más tarde y queremos asegurarnos que todas hayan acabado antes de hacer nada. Para estos casos tenemos "Promise.all".
La clase Promise de Javascript tiene un método llamado all() que recibe un array de promesas. Las ejecuta todas y, cuando acaba la última, se devuelve un array con todos los valores devueltos por las promesas.
Promise.all([
esperar(5000),
esperar(1000),
esperar(3000)
]).then( respuestas => {
console.log("Fin!");
});
Ahora el mensaje "Fin" se mostrará por último, una vez acaben todas las promesas. Por tanto, y dado que se ejecutan en paralelo, tardará 5 segundos en total en aparecer "Fin".
Si quisiéramos trabajar con los valores de devolución de las promesas, podemos obtenerlos con el array de respuestas que nos devuelve Promise.all(). Por ejemplo así mostraríamos todas las respuestas haciendo un recorrido al array.
Promise.all([
esperar(5000),
esperar(1000),
esperar(3000)
]).then( respuestas => {
for (let i in respuestas) {
console.log(respuestas[i]);
}
}
);
Conclusión
Las promesas son ideales para la programación asíncrona, porque mantienen el orden en el código y la sencillez, con lo que también tus programas serán más claros y fáciles de extender o depurar. En este artículo has podido aprender a trabajar con promesas, además de crear tus propias implementaciones de funciones que devuelven promesas. Con todo ello serás capaz de sacarles un gran partido a estas herramientas para la mejora del código.
Sin embargo, para manejar promesas de una manera más avanzada hemos podido aprender a gestionar varias a la vez, tanto en secuencia (útil cuando la ejecución de una promesa depende del resultado producido por la anterior) como en paralelo (de manera que se realicen todas a una y ahorremos tiempo), algo que seguramente te será de utilidad más de una vez en tus aplicaciones Javascript.
Miguel Angel Alvarez
Fundador de DesarrolloWeb.com y la plataforma de formación online EscuelaIT. Com...