====== Promises ======
Módulo perteneciente al curso [[informatica:programacion:cursos:programacion_avanzada_javascript|Programación avanzada con JavaScript y ECMAScript]].
===== Introducción =====
Las //promises// (promesas) ya hace algún tiempo que se vienen utilizando en el desarrollo JavaScript, aunque no es hasta ECMAScript 2015 cuando se incorporan "de serie" en el lenguaje. De todos modos, si debes soportar algún navegador que no las incorpora (básicamente IE en cualquier versión, Edge -incluido en Windows 10- sí que las soporta) [[https://github.com/jakearchibald/es6-promise|puedes usar algún polyfill]].
La especificación de Promises en ECMAScript 2015 es conocida como Promises/A+
==== ¿Qué es una promise? ====
Una //promise// es, como su propio nombre indica, **una promesa**: una promesa de que cierta tarea se finalizará en algún momento. Así pues, debes ver a las promises como tareas "que serán ejecutadas en algún momento dado". Esto incluye el pasado, es decir, **una //promise// puede representar una tarea realizada, realizándose o por realizar** (pero que se realizará).
A priori puede costar ver la necesidad de tener algo como las promises, pero para ver su potencial es necesario entender antes el concepto de la "pirámide de callbacks".
==== La pirámide de callbacks ====
Tomemos como punto de partida el siguiente código, ¡a ver si encuentras qué tiene de malo!:
navigator.geolocation.getCurrentPosition(function(c) {
console.log(c);
}
);
La respuesta es: nada. Este código no tiene nada de malo. Estamos llamando al método ''getCurrentPosition'' y pasándole una función de //callback//. Hacemos esto continuamente en JavaScript. El problema puede venir si dentro de la función de //callback// tenemos que llamar a otra función que espera a su vez un //callback//, tal y como sucede en el siguiente código:
navigator.geolocation.getCurrentPosition(function(c) {
console.log(c);
window.setTimeout(function() {
navigator.geolocation.getCurrentPosition(function(c2) {
console.log(c2);
});
}, 2000);
}
);
**callback**: Un método que se llama automáticamente al terminar de ejecutarse otro método anterior. Por ejemplo, se llama a una función que procesa unos datos y cuando ésta termina, llama a un método que le hayamos indicado en sus argumentos, para iniciar un proceso diferente o para indicar que ha finalizado.
Observa que dentro del //callback// del primer ''getCurrentPosition'' se llama a ''setTimeout'' que recibe otro //callback//, y a su vez dentro de este segundo //callback// se llama otra vez a ''getCurrentPosition'' y se le pasa una tercera función de //callback//.
Si te fijas en la estructura de código, debido a la indentación, parece una pirámide con la punta hacia la derecha. Esta sucesión de unos callbacks dentro de otros una y otra vez es lo que se conoce como la "pirámide de callbacks" y puede llegar a extremos enfermizos:
{{ https://miro.medium.com/v2/resize:fit:640/format:webp/1*3lEILqKvoasyVwpdlfVvbw.png |}}
[[https://medium.com/@bmlf72/to-survive-the-pyramid-of-doom-4e8ce4fb5d6b|Fuente de la imagen]]
Las //promises// pueden ayudar a solventar este problema, aunque se debe evitar caer en la "**pirámide de //promises//**", que también existe. Pero también veremos otras posibilidades que ofrecen.
===== Desmontando la pirámide de callbacks =====
Vamos a ver cómo el uso de //promises// nos permite desmontar la pirámide de //callbacks// que teníamos en el ejemplo anterior. Para ello vamos a suponer que las APIs que usábamos están implementadas usando //promises//. Simplemente, ten presente que esto no es cierto: no hay todavía APIs nativas en JavaScript que estén implementadas usando //promises//.
Para este ejemplo vamos a suponer que existen las funciones ''getCurrentPositionPromise'' y ''setIntervalPromises'' que hacen lo mismo que las funciones reales ''getCurrentPosition'' y ''setInterval'' pero usando //promises//. Más adelante veremos cómo podemos crear realmente esas funciones.
Una API basada en //promises// en lugar de esperar un //callback// devuelve una //promise//. Así pues, nuestro supuesto método ''getCurrentPosition'' no esperaría un parámetro con la función de //callback//, sino que devolvería una //promise//, por lo que sería llamado de la siguiente manera:
var pr = navigator.geolocation.getCurrentPositionPromise();
El valor devuelto (''pr'') es la //promise//. Recuerda que una //promise// representa una tarea que ya puede estar hecha o que se realizará en un tiempo futuro. Para saber el estado de la tarea debemos consultar el estado de la //promise//, el cual puede ser:
* **Pendiente** (''pending''): La tarea no se ha ejecutado y no hay resultado alguno.
* **Completada** (''fulfilled''): La tarea ha sido completada y por lo tanto hay un resultado obtenido. Este resultado (que puede ser ''undefined'') queda asociado a la //promise//.
* **Error** (''rejected''): Ha habido un error ejecutando la tarea y por lo tanto no hay resultado alguno que podamos consultar.
En realidad **no existe ninguna propiedad que nos diga el estado** en el que se encuentra una //promise//. En su lugar se usan **dos métodos** de la //promise//: el método **then** y el método **catch**.
El primero, ''then'', recibe como parámetro una función, que **se ejecutará en cuanto la tarea sea completada sin error**. Por su parte, el segundo método, ''catch'', recibe como parámetro una función que **se ejecutará si hay algún error al ejecutar la tarea**.
Para entendernos, cuando la //promise// pasa de estado "Pendiente" a "Completada" se ejecutará la función pasada como parámetro en el método ''then'', y cuando la //promise// pasa de "Pendiente" a "Error" se ejecutará la función pasada como parámetro en el ''catch''.
Pero no solo eso, **si llamas a ''then'' o a ''catch'' de una //promise// que ya se encuentra en estado "Completada" o "Error", se ejecutará la función pasada correspondiente**. Es decir, da igual si la tarea representada por la //promise// ha terminado o no cuando llames a ''then'': si no lo ha hecho, tan pronto termine se ejecutará la función parámetro del ''then'', y si ya había finalizado entonces se ejecutará de inmediato la función pasada como argumento en el ''then''.
Por lo tanto para simular el ejercicio anterior con //promises// usaríamos un código similar a:
var pr = navigator.geolocation.getCurrentPositionPromise();
pr.then(function(coords) {
console.log(coords);
var pr2 = setIntervalPromises(2000);
pr2.then(function() {
var pr3 = navigator.geolocation.getCurrentPositionPromise();
pr3.then(function(coords2) {
console.log(coords2);
});
});
});
Observa que la función que se le pasa al ''then'' recibe como argumento el valor devuelto por la tarea representada por la //promise//.
Este código, sin embargo, no es mucho mejor que el anterior. Es cierto que ahora no tenemos una "pirámide de callbacks", pero tenemos una "pirámide de promises". Esta otra pirámide se da cuanto tenemos thens dentro de thens dentro de thens de forma sucesiva (en este caso encadenamos tres thens uno dentro del otro). Eso es porque en este ejemplo estamos usando mal las promises.
Evita siempre caer en la pirámide de //promises//: tiene los mismos problemas de legibilidad y mantenibilidad que la pirámide de //callbacks//.
==== Rompiendo la pirámide de promises ====
Para poder romper la pirámide de //promises// el primer paso es separar cada una de las tareas en funciones separadas:
// Tarea 1: Obtener la posición (e imprimirla en consola)
var task1 = function() {
var pr = navigator.geolocation.getCurrentPositionPromise();
pr.then(function(c) {
console.log('1ª coord -> ', c);
});
return pr;
}
// Tarea 2: Esperar 2000ms (y imprimir por consola una vez han pasado)
var task2 = function() {
var pr = setIntervalPromises(2000);
pr.then(function() {
console.log('2000 ms de espera');
});
return pr;
}
// Tarea 3: Obtener la posición (e imprimirla en consola)
var task3 = function() {
var pr = navigator.geolocation.getCurrentPositionPromise();
pr.then(function(c) {
console.log('2ª coord -> ', c);
});
return pr;
}
Observa que las funciones hacen lo que tengan que hacer, pero devuelven una //promise//, puesto que representan tareas. En este caso nos limitamos a devolver la misma //promise// que nos devuelven los métodos ''getCurrentPositionPromise'' y ''setIntervalPromises''.
Ahora necesitamos encadenar las 3 tareas una tras otra. Para ello, no debemos usar un código como el siguiente:
var pr = task1().then(
function() {
task2().then(function() {
task3();
});
});
¡Este código **funciona correctamente pero sigues teniendo la pirámide de //promises//!** El mero hecho de tener las tareas separadas en funciones no te sirve para evitar la pirámide de //promises// si luego vuelves a encadenar los thens uno dentro de otro.
Para poder romper la pirámide de promises debes tener presente una cosa: **los métodos ''then'' se pueden encadenar** entre ellos. De hecho el método ''then'' devuelve otra //promise// cuyo estado depende del valor devuelto por la función que se pasa al ''then''. Así podemos tener el siguiente código:
task1().
then(task2).
then(task3).
then(function() {
console.log('Todas las Promises resueltas.');
});
Observa la elegancia de este código: ya no hay ''thens'' dentro de ''thens'', ni nada parecido. En su lugar el código es muy claro y descriptivo. Puedes leerlo de la siguiente manera:** ejecutar ''task1'' y luego ''task2'' y luego ''task3'' y cuando ''task3'' haya terminado ejecutas el último ''then''**.
¡Más claro y sencillo, no puede ser!
Este código funciona porque cada método ''then'' devuelve una promise, por esto los podemos encadenar.
Pero te estarás preguntando: ¿cuándo se resuelve la //promise// que devuelve el método ''then''?. Pues la respuesta es muy simple:
- En el caso de que la función pasada al método ''then'' devuelva una //promise//, entonces cuando se resuelva esta última se resolverá también la //promise// devuelta por el método ''then''. A todos los efectos es como si then retornase la misma //promise// que retorna la función que se le pasa como parámetro. A este hecho se le conoce como //promise unwrapping//.
- En el caso de que la función pasada a ''then'' no devuelva una //promise// entonces la //promise// devuelta por ''then'' estará en estado resuelta.
- En el caso de que la función que se pasa como parámetro a ''then'' lance un error, entonces la //promise// devuelta por ''then'' estará en estado de error.
Volvamos a ver el código que encadena los ''thens'' pero ahora con comentarios para entender mejor qué ocurre a cada paso:
task1(). // En este punto tenemos una promise, llamémosla 'pr1'
then(task2). // llamamos a then de pr1. Esto ejecutará pr2.
// task2 devuelve una promise, llamémosla pr2.
// En este caso el método then devuelve otra promise cuyo estado depende
// del estado de pr2. Es como si el método then devolviese la misma pr2.
then(task3). // Por lo tanto esta línea equivale a pr2.then(...). Por lo que este then
// se ejecutará cuando pr2 se haya resuelto.
// Igualmente task3 devuelve una promise (llamémosla pr3),
// por lo que el método then(task3) devuelve una promise
// cuyo estado depende de pr3
then(function() { // Este then pues se ejecutará cuando pr3 se haya resuelto.
console.log('Todas las Promises resueltas.');
});
==== Encadenamiento de then ====
Vamos a ver algunos ejemplos del encadenamiento de métodos ''then'' que hemos comentado. Para cada ejemplo hay una captura de pantalla de la consola de Firefox para que veas el estado de los objetos.
El primer ejemplo muestra como ''then'' devuelve siempre una //promise//:
var pr = new Promise(function(r,j) { r(); });
var p1 = pr.then(function() {
console.log('promise resolved');
});
{{ :informatica:programacion:cursos:programacion_avanzada_javascript:10-then-devuelve-promise-01.png |}}
Observa como ''p1'' es una //promise// a pesar de que la función pasada por ''then'' no devuelve nada. Por ello la //promise// ''p1'' está resuelta y su valor asociado es ''undefined''
En este segundo ejemplo se puede observar como la //promise// devuelta por ''then'' está en estado de error si la función que se pasa al ''then'' lanza una excepción:
var pr = new Promise(function(r,j) { r(); });
var p2 = pr.then(function() {
throw("some error!");
});
{{ :informatica:programacion:cursos:programacion_avanzada_javascript:10-then-devuelve-promise-error.png |}}
Y el tercer ejemplo muestra que, si la función que se pasa a ''then'' devuelve una //promise//, la promise devuelta por ''then'' se resuelve en cuanto se resuelve la //promise// devuelta por la función que se pasa a ''then'':
var pr = new Promise(function(r,j) { r(); });
var p1 = pr.then(function() {
return new Promise(function(r,j) {
window.setTimeout(function() {
console.log("promise inside then is resolved");
r();
}, 5000);
});
});
var p2=p1.then(function() { console.log("p1 resolved too");});
Hasta que no se resuelva la //promise// "interna" (cuando pasan los 5 segundos del ''setTimeout'') no se resuelve ''p1'' (la //promise// devuelta por el ''then''), de ahí que salga primero el mensaje "//promise inside then is resolved//" y luego el mensaje "//p1 resolved too//":
{{ :informatica:programacion:cursos:programacion_avanzada_javascript:10-then-devuelve-promise-interna.png |}}
Con estos ejemplos espero que quede más claro cómo funciona el encadenamiento de //promises// y cómo el método ''then'' devuelve siempre una promise.
===== Creación de promises =====
Vamos a ver cómo crear nuestras propias funciones que utilicen y devuelvan //promises//. Para ello debemos ver cómo crear objetos del tipo ''Promise''.
La función constructora ''Promise'' acepta un solo parámetro, que se conoce como la "**función ejecutora**". Dicha función contiene el código de la tarea que la //promise// encapsula:
var p = new Promise(function() {
console.log('Hello promises');
});
El código que se coloca dentro de la función ejecutora se ejecuta inmediatamente. Es decir, el código anterior imprime la cadena "//Hello promises//" por la consola. El objeto ''p'' es la //promise//. En este caso este código sirve de poco porque la //promise// ''p'' está en estado pendiente, y no hay manera de resolverla (pasarla a estado completado) o rechazarla (pasarla a estado de error).
El **resolver o rechazar una //promise// debe hacerse desde dentro de la función ejecutora**.
Dicha función recibe dos parámetros (por regla general llamados ''resolve'' y ''reject'') que son sendas funciones que al llamarlas nos permiten resolver o rechazar la //promise//. P. ej. el siguiente código devuelve una //promise// completada:
var p = new Promise(function(resolve, reject) {
console.log('Hello promises');
resolve();
});
En este punto, la variable ''p'' es una //promise// en estado completado. Así podríamos tener una función ''foo'' que nos permitiese obtener //promises// (ya completadas):
var foo = function() {
var p = new Promise(function(resolve, reject) {
console.log('Hello promises');
resolve();
});
return p;
};
Y ahora podríamos tener el siguiente código:
foo().then(function() { console.log('Hello after promises')});
Esto imprimiría por la consola primero la cadena "//Hello promises//" y luego la cadena "//Hello after promises//". Fíjate en que ''foo'' devuelve la //promise// (ya completada), por lo que la siguiente llamada a ''then'' se ejecuta inmediatamente. Por supuesto este ejemplo es muy sencillo y usar //promises// en este escenario no aporta nada. Lo interesante viene cuando la función ''foo'' espera un //callback//, por la razón que sea. Los //callbacks// suelen usarse cuando hay operaciones asíncronas aunque no siempre tiene porque ser así.
==== Encapsulando APIs asíncronas con promises ====
Vamos a ver un ejemplo más interesante. En este caso encapsularemos la API ''getCurrentPosition'' para usar //promises// en lugar de //callbacks//. Primero definimos nuestra nueva función, que llamaremos ''getCurrentPositionPromise''. Esta función llamará a ''getCurrentPosition'' y le pasará un //callback//. Dentro de éste resolveremos la //promise//. Parece complicado así explicado pero es muy simple:
navigator.geolocation.getCurrentPositionPromise = function() {
var pr = new Promise(function(resolve, reject) {
navigator.geolocation.getCurrentPosition(function(c) {
resolve(c); // Resolvemos la promise
});
});
return pr; // Devolvemos la promise
}
La función ''getCurrentPositionPromise'' envuelve a la función nativa ''getCurrentPosition'' usando //promises// en lugar de //callbacks//. Por lo tanto ahora podemos tener el siguiente código:
var task = navigator.geolocation.getCurrentPositionPromise();
task.then(function(c) {
console.log("Done -> ",c);
});
Ejercicio: Completa la función ''getCurrentPositionPromise'' para que tenga presente que la función nativa ''getCurrentPosition'' puede dar un error y en este caso que pase la //promise// al estado de error (llamando a ''reject''). En el fichero ''setInterval''. Tienes la solución en el fichero ''encapsular2.js''.
Fichero ''encapsular2.js'':
var setIntervalPromises = function(interval) {
var pr = new Promise(function(resolve, reject) {
window.setInterval(function() { resolve(); },
interval);
});
return pr; // Devolvemos la promise
}
// uso:
// var wait = setIntervalPromises(interval)
// wait.then(function() { ... });
===== DEMO: Creación y uso de promises =====
Partimos de lo siguiente:
var demo = function(res, cb) {
window.setTimeout(function() { cb(res); }, 2000 );
}
Para invocarla:
demo(10, function(r) {
console.log("Recibido ->", r);
});
Vamos a transformar esa función para utilizar una //promise// y así no tener que usar //callbacks//.
var demo = function(res) {
var pr = new Promise(function(resolve, reject) {
window.setTimeout(function() { resolve(res); }, 2000 );
});
return pr;
}
Para utilizarla:
var task = demo(10);
task.then(function(r) {
console.log("Recibido ->", r);
})
El resultado será el mismo que antes, pero ya no hay ninguna función de //callback// y queda más separado la llamada de la función de lo que ocurre cuando la función finaliza.
===== DEMO: Encadenar promises =====
Veamos cómo podemos hacer una secuencia de //promises// y se ejecuten una tras otra.
Partimos de la siguiente función que esperaba un tiempo y luego resuelve la //promise// con un resultado.
var demo = function(res) {
var pr = new Promise(function(resolve, reject) {
if (res < 0) {
reject(new Error("El resultado debe ser positivo"));
}
window.setTimeout(function() { resolve(res)}, 2000);
});
return pr;
}
Siempre que queramos encadenar tareas, hay que definir cada una de esas tareas en una función.
Primera tarea:
var task1 = function() {
console.log("Iniciando tarea 1");
return demo(100);
}
Segunda tarea:
var task2 = function() {
console.log("Iniciando tarea 2");
return demo(2000);
}
Para encadenar, primero llamamos a la primera tarea y luego, como la primera tarea devuelve una promise, cuando termine esta, llamaremos a la segunda tarea:
console.log("Inicio");
task1().then(task2).then(function() {
console.log("Fin de todas las tareas");
});
===== DEMO: Encadenar Promises y tratar el resultado =====
En el apartado anterior hemos visto cómo encadenar las //promises//, pero no hemos hecho nada con los resultados. Cada una devolvía un resultado. Veamos cómo podemos tratarlos.
Primero tenemos que ver cómo la tarea 2 puede saber cuál es el resultado de la tarea 1. Basta con que lo reciba como parámetro:
var task2 = function(rp) {
console.log("El resultado previo es ->" , rp);
console.log("Iniciando tarea 2");
return demo(2000);
}
Para tratar el resultado desde la propia tarea:
var task1 = function() {
console.log("Iniciando tarea 1");
return demo(100).then(function(r) {
console.log("En task1() el resultado es ", r);
});
}
Sin embargo, perdemos el resultado en ''task2'' por haber usado el ''then'' en ''task1''. Modificamos ''task1'' para devolver el resultado:
var task1 = function() {
console.log("Iniciando tarea 1");
return demo(100).then(function(r) {
console.log("En task1() el resultado es", r);
return r;
});
}
Todo el código junto:
var demo = function(res) {
var pr = new Promise(function(resolve, reject) {
if (res < 0) {
reject(new Error("El resultado debe ser positivo"));
}
window.setTimeout(function() { resolve(res)}, 2000);
});
return pr;
}
var task1 = function() {
console.log("Iniciando tarea 1");
return demo(100).then(function(r) {
console.log("En task1() el resultado es", r);
return r;
});
}
var task2 = function(rp) {
console.log("El resultado previo es ->" , rp);
console.log("Iniciando tarea 2");
return demo(2000);
}
// Uso:
console.log("Inicio");
task1().then(task2).then(function() {
console.log("Fin de todas las tareas");
});
===== Composición de promises =====
Una idea interesante de las promises es que pueden crearse **//promises// compuestas**, que **solo se resuelven cuando todas las //promises// que las componen se resuelven** a su vez (o que entran en el estado de error, en cuanto una de las promises que las componen entra en estado de error).
Para esta lección, se supone la existencia de una función llamada ''xhrget'' a la que se le pasa un parámetro (una cadena con URL) y usando ''XMLHttpRequest'' realiza una llamada AJAX GET a la URL indicada y devuelve una promise que se resuelve tan pronto como la petición AJAX está realizada. Se propone como ejercicio el realizar dicha función. En el fichero ''then'' puedes conseguir lo propuesto, con la salvedad de que estás forzando a que las 3 peticiones se ejecuten una tras otra de forma secuencial, cuando no hay necesidad de ello. En estos casos es cuando las //promises// compuestas son extremadamente útiles. Y, si nunca has tenido que resolver escenarios similares sin //promises//, usando variables contadoras y demás, apreciarás todavía más la simplicidad de las //promises// compuestas.
==== Promise.all ====
Para crear una //promise// compuesta, se usa el método ''Promise.all''. A este método se le pasa un "iterable" (p. ej. un array) de //promises// y nos devuelve la //promise// compuesta. Tan simple como:
var p1 = xhrget('http://www.campusmvp.es/');
var p2 = xhrget('http://www.campusmvp.es/catalogo/');
var p3 = xhrget('http://www.campusmvp.es/opiniones-campusmvp.htm');
var pall = Promise.all([p1,p2,p3]);
pall.then(function() {
// En este punto las TRES promises (p1, p2 y p3) están resueltas.
});
El concepto de "iterable" es nuevo en ECMAScript 2015. Básicamente por ahora podemos considerar que los iterables son arrays, aunque hay otros tipos que también lo son y más adelante veremos cómo hacer que nuestros propios objetos se comporten como iterables si así lo deseamos.
Por supuesto queda por aclarar un punto: cómo obtenemos los valores asociados a las distintas //promises// en el ''then'' de la //promise// compuesta.
El método ''then'' de una //promise// compuesta **recibe como parámetro un array que contiene todos los resultados de las //promises// que la componen**.
En nuestro caso dado que la //promise// devuelta por la función ''xhrget'' tiene como valor el propio objeto ''XMLHttpRequest'' usado, el método ''then'' de la //promise// compuesta recibe como parámetro un array de 3 elementos ''XMLHttpRequest''.
El siguiente código ilustra con más claridad este concepto:
var p1 = new Promise(function(r,j) { r('one');}); // resolvemos con 'one'
var p2 = new Promise(function(r,j) { r(42);}); // resolvemos con 42
var p3 = new Promise(function(r,j) { r({foo:'bar'});}); // resolvemos con {foo:'bar'}
Promise.all([p1,p2,p3]).then(function(values) { console.log(values)}); // ['one', 42, {foo:'bar'}]
{{ :informatica:programacion:cursos:programacion_avanzada_javascript:10-promise-all.png |}}
===== DEMO: Composición de Promises =====
Veremos cómo podemos ejecutar //promises// todas a la vez, de forma paralela.
Partimos de:
var demo = function(res) {
var pr = new Promise(function(resolve, reject) {
if (res < 0) {
reject(new Error("El resultado debe ser positivo"));
}
window.setTimeout(function() { resolve(res)}, 2000);
});
return pr;
}
var task1 = function() {
console.log("Iniciando tarea 1");
return demo(100).then(function(r) {
console.log("En task1() el resultado es ', r);
return r;
});
}
var task2 = function(rp) {
console.log("El resultado previo es ->" , rp);
console.log("Iniciando tarea 2");
return demo(2000);
}
console.log("Inicio");
Promise.all([task1(), task2()]).then(function () {
console.log("Fin", arguments[0]);
});
''arguments[0]'' es un array con los resultados de todas las //promises// que forman la //promise// paralela.
Esto va muy bien para sincronizar peticiones AJAX. Este método nos ahorra utilizar contadores o variables globales.
===== Captura de errores en promises =====
Cuando una //promise// entra en estado de error se invoca a su método ''catch''. El método ''catch'' es el equivalente a ''then'', pero se ejecuta cuando la //promise// entra en estado de error en lugar de cuando se resuelve.
La función ejecutora de una //promise// utiliza su segundo parámetro (usualmente llamado ''reject'') para pasar la //promise// a estado de error:
var errored = new Promise(function(resolve, reject) {
reject(new Error("Some error hapened"));
});
console.log(errored);
En este código la promise ''errored'' está en estado de error. Eso significa que su método ''then'' no se va a ejecutar nunca (dado que no está resuelta ni se va a poder resolver). El método que se va a ejecutar es el método ''catch'':
errored.catch(function(err) {
console.log(err);
});
{{ :informatica:programacion:cursos:programacion_avanzada_javascript:10-promise-catch.png |}}
El método ''catch'' comparte todas las otras características del método ''then'':
* Si ejecutas ''catch'' sobre una //promise// que YA está en estado de error, el método se ejecuta al momento.
* La función que se pasa como parámetro a ''catch'' recibe como parámetro el valor que la función ejecutora de la //promise// pasó a ''reject''. (Observa como en el ejemplo, el valor del parámetro ''err'' es el objeto ''Error'' que se ha pasado como parámetro al ''reject'').
* El método ''catch'' devuelve otra //promise// cuyo estado depende de la función que se pase a este ''catch'' (con exactamente las mismas normas que en el caso del método ''then'').
===== DEMO: Errores en promises =====
Vamos a forzar un error, es decir, que se devuelva una promise en estado de error.
var demo = function(res) {
var pr = new Promise(function(resolve, reject) {
if (res < 0) {
// Pasamos la promise a estado de error
reject(new Error("El resultado debe ser mayor que cero"));
}
// Por si se generase algún otro tipo de error, lo capturamos
try {
window.setTimeout(function() { resolve(res)}, 2000);
}
catch (e) {
reject(e);
}
});
return pr;
}
Creamos la tarea y capturamos el error:
var task = demo(-10); // Forzamos el error
// Nunca va ejecutará porque la promise no
// pasará a estado resuelto
task.then(function(r) {
console.log("Recibido -> ", r);
}
// Capturamos el error
task.catch(function(e) {
console.log("Error recibido -> ", e);
});
console.log("Fin");
===== ¿Cómo limitar el tiempo máximo de ejecución de una Promise? =====
Si la API que usamos dentro de la //Promise// ofrece algún mecanismo de //timeout//, lo ideal es usarlo, pero si no es el caso (como curiosamente ocurre con ''fetch'') existe un mecanismo muy sencillo para conseguirlo: usar ''Promise.race''. Esta función toma un iterable de //promises// y devuelva otra promesa que se resuelve/rechaza en cuanto una de las promesas del iterable se resuelva o se rechace.
Por lo tanto el mecanismo es muy sencillo. Esta función toma una Promise y devuelve otra con un timeout:
const TimeoutPromise = (pr, timeout) =>
Promise.race([pr, new Promise((_, rej) =>
setTimeout(rej, timeout)
)]);
Su uso es muy sencillo:
let pr = fetch('http://slowwly.robertomurray.co.uk/delay/8000/url/http://www.google.co.uk', {mode: 'no-cors'});
let tpr = TimeoutPromise(pr, 500)
.then(() => console.log('fetch done'))
.catch(() =>console.log('timeout cancelled'));
La //promise// ''pr'' es una promise que tarda 8 segundos en resolverse (es lo que tarda en cargar esa página), pero si ejecutas ese código verás que al cabo de 500ms la promise ''tpr'' es rechazada.
Más fácil imposible, ¿verdad?
Bien, nada es perfecto: este método cuando se rechaza la promise por el timeout, **el resto de promises internas siguen ejecutándose**. Es decir, en nuestro caso, tpr es rechazada al cabo de 500ms, pero la promise pr se sigue ejecutando durante los 8s (hasta que se completa la petición de red). Por lo tanto **este mecanismo no aborta promises**. Se limita a envolverlas con una promesa que, esta sí, se resuelve/rechaza en un tiempo máximo.
Lo ideal sería poder cancelar la promise, pero ECMAScript no ofrece un mecanismo para poder hacerlo, aunque [[https://github.com/tc39/proposal-cancellation|el TC39 está trabajando en ello]].
==== "Cancelando" promises ====
Existen [[https://medium.com/@benlesh/promise-cancellation-is-dead-long-live-promise-cancellation-c6601f1f5082|otras técnicas]], pero si para ti es vital el poder "cancelar promises" aquí tienes un código muy sencillo que permite **interrumpir una promise en cada uno de sus pasos**. Cada uno de esos pasos debe ser a su vez una promise:
const token = function() {
return {
cancel: function () {this.isCancelled = true},
isCancelled: false
}
}
async function MultistepPromise (iterf, token)
{
for (let f of iterf) {
await f();
if (token.isCancelled) {
throw 'promise cancelled';
}
}
}
Bueno, como puedes ver el código es trivial: simplemente itera por el iterable de promises si por alguna razón el token se cancela.
Su uso es, de nuevo, muy sencillo:
const func = () => fetch(
'http://slowwly.robertomurray.co.uk/delay/3000/url/http://www.google.co.uk',
{mode: 'no-cors'});
const tok = token();
const pr = MultistepPromise([func, func, func, func, func, func], tok)
.then(() => console.log('all acceoted'))
.catch((e) => console.log('some error', e));
Si una vez ''pr'' se está ejecutando, llamas a ''tok.cancel()'', entonces ''pr'' se cancelará una vez se haya finalizado el paso correspondiente (a medio paso no se puede cancelar).
Observa que ''MultistepPromise'' no espera un array de promises, sino simplemente un iterable de promises, por lo que puedes usar otras técnicas tales como un generador para pasarle las promises a ejecutar.
Por supuesto, puedes combinar ambas técnicas: es decir, hacer una ''MultistepPromise'', donde cada uno de sus pasos sea ''TimeoutPromise''. De este modo te aseguras que si uno de los pasos excede el tiempo, todo el resto de pasos se cancelan (no se ejecutan).
===== Recursos =====
* [[https://promisesaplus.com/|Especificación oficial de Promises/A+]]
* [[https://github.com/stefanpenner/es6-promise|Polyfill de Promises/A+]]
* [[https://dmitripavlutin.com/javascript-promises-then-vs-then-catch/|JavaScript Promises: then(f,f) vs then(f).catch(f)]]