8. Promesas

Las promesas son una característica novedosa para gestionar los eventos asíncronos, permitiendo escribir código más sencillo, callbacks más cortos, y mantener la lógica de la aplicación de alto nivel separada de los comportamientos de bajo nivel.

Al utilizar promesas, podemos usar callbacks en cualquier situación, y no solo con eventos. Además ofrecen un mecanismo estándar para indicar la finalización de tareas.

8.1. ¿Qué es una promesa?

Una promesa es un objeto que representa un evento único, normalmente como resultado de una tarea asíncrona como una llamada AJAX. Almacenan un valor futuro, ya sea el resultado de una petición HTTP o la lectura de un fichero desde disco.

En la vida real, cuando vamos a un restaurante de comida rápida y pedimos un menú con hamburguesa con bacón y patatas fritas, estamos obteniendo una promesa con el número del pedido, porque primero lo pagamos y esperamos recibir nuestra comida, pero es un proceso asíncrono el cual inicia una transacción. Mientras esperamos a que nos llamen con nuestra sabrosa hamburguesa, podemos realizar otras acciones, como decirle a nuestra pareja que busque una mesa o preparar un tuit con lo rica que está nuestra hamburguesa. Estas acciones se traducen en callbacks, los cuales se ejecutarán una vez se finalice la operación. Una vez nuestro pedido está preparado, nos llaman con el número del mismo y nuestra deseada comida. O inesperadamente, se ha acabado el bacón y nuestra promesa se cumple pero erroneamente. Así pues, un valor futuro puede finalizar correctamente o fallar, pero en ambos casos, finalizar.

8.2. Promise API

Las promesas forman parte del API de JavaScript desde ECMAScript 6, con lo cual es una tecnología novedosa que puede no estar disponible en todos los navegadores. Aún así, las últimas versiones de los navegadores ya lo soportan: http://caniuse.com/#feat=promises

Además de la implementación del estándar ECMAScript 6, jQuery implementa las promesas mediante los Deferreds, y existen librerías de terceros como BlueBird (https://github.com/petkaantonov/bluebird) o Q (https://github.com/kriskowal/q) que cumplen el estándard.

8.2.1. Hola Promesa

Para crear una promesa, utilizaremos el contructor Promise(), el cual recibe como argumento un callback con dos parámetros, resolver y rechazar. Este callback se ejecuta inmediatamente.

Para facilitar la lectura del código, las funciones resolver y rechazar están traducidas. El API de promesas utiliza resolve y reject como alternativa, y además, ofrece métodos auxiliares para realizar estas acciones con dicha nomenclatura.

Dentro de la promesa, realizaremos las acciones deseadas y si todo funciona como esperamos llamaremos a resolver. Sino, llamaremos a rechazar (mejor pasándole un objeto Error para capturar la pila de llamadas)

var promesa = new Promise(function(resolver, rechazar) {
  var ok;

  // código con la llamada async

  if (ok) {
    resolver("Ha funcionado"); // resuelve p
  } else {
    rechazar(Error("Ha fallado")); // rechaza p
  }
});

Así pues, los parámetros resolver y rechazar tienen la capacidad de manipular el estado interno de la instancia de promesa p. Esto se conoce como el patrón Revealing Constructor (http://blog.domenic.me/the-revealing-constructor-pattern/), ya que el constructor revela sus entrañas pero solo al código que lo construye, el cual es el único que puede resolver o rechazar la promesa. Por ello, cuando le pasamos la promesa a cualquier otro método, éste solo podrá añadir callbacks sobre la misma:

hacerCosasCon(promesa);

8.2.2. Uso básico

Una vez tenemos nuestra promesa, independientemente del estado en el que se encuentre, mediante then(callbackResuelta, callbackRechazada) podremos actuar en consecuencia dependiendo del callback que se dispare:

promesa.then(
  function(resultado) {
    console.log(resultado); // "Ha funcionado"
  }, function(err) {
    console.error(err); // Error: "Ha fallado"
  }
);

El primer callback es para el caso completado, y el segundo para el rechazado. Ambos son opcionales, con lo que podemos añadir un callback para únicamente el caso completado o rechazado.

La especificación describe el término thenable para aquelos objetos que son similares a una promesa, ya que contienen un método then.

Uno de los ejemplos más sencillos es una petición AJAX mediante jQuery:

var p = $.get("http://www.omdbapi.com/?t=Interstellar&r=json");

p.then(function(resultado) {
  console.log(resultado);
});

8.2.3. Estados de una promesa

Nada más comenzar, una promesa está en un estado pendiente (pending). Al finalizar, su estado será completada (fulfilled), lo que implica que la tarea se ha realizado/resuelto, o rechazada (rejected), si la tarea no se completó.

Estados de una promesa
Figure 1. Estados de una Promesa
No hay que confundir completada con éxito, ya que una promesa se puede completar pero contener un error (por ejemplo, el usuario solicitado no existe), o puede no completarse porque el usuario cancelase la petición incluso sin ocurrir ningún error.

Una vez que una promesa se completa o se rechaza, se mantendrá en dicho estado para siempre (conocido como settled), y sus callbacks nunca se volverán a disparar. Tanto el estado como cualquier valor dado como resultado no se pueden modificar.

Dicho de otro modo, una promesa sólo puede completarse o rechazarse una vez. No puede hacerlo dos veces, ni cambiar de un estado completado a rechazado, o viceversa.

Autoevaluación

A partir del siguiente código anterior, ¿Qué mensajes saldrán por pantalla? [1]

var promesa = new Promise(function (resolver, rechazar) {
  resolver(Math.PI);
  rechazar(0);
  resolver(Math.sqrt(-1));
});

promesa.then(function (num) {
  console.log("El número es " + num)
})

Es decir, se queda en un estado inmutable, el cual se puede observar tantas veces como queramos. Por ello, una vez resuelta, podemos pasar tranquilamente la promesa a cualquier otra función, ya que nadie va a poder modificar su estado.

Y ahora viene la pregunta del millón: Si la función rechazar pasa una promesa al estado rechazado, ¿por qué la función resolver no pasa la promesa al estado resuelta en vez de al estado completada? La respuesta corta es que resolver una promesa no es lo mismo que completarla. La larga es que cuando a la función resolver se le pasa un valor, la promesa se completa automáticamente. En cambio, cuando se le pasa otra promesa (por ejemplo promesa.resolver(otraPromesa)), las promesas se unen para crear una única promesa, de manera que cuando se resuelve la 2ª promesa (otraPromesa), ambas promesas se resolverán. Del mismo modo, si se rechaza la 2ª promesa, las dos promesas se rechazarán.

El argumento que se pasa a resolver decide el destino de la promesa. Si es un valor, se completa. Si es una promesa, su estado depende de ésta última.

Tanto resolver como rechazar se pueden llamar sin argumentos, en cuyo caso el valor de la promesa será undefined.

Finalmente, si queremos crear una promesa que inmediatamente se resuelva o rechace, podemos emplear los métodos Promise.resolve() o Promise.reject():

Funciones equivalentes para resolver y rechazar
new Promise(function(resolver, rechazar) {
  resolver("forma larga");
});
Promise.resolve("forma corta");

new Promise(function(resolver, rechazar) {
  reject("rechazo larga");
});
Promise.reject("rechazo corta");

Estos métodos son útiles cuando ya tienes el elemento que debería resolver o rechazar la promesa.

8.2.4. Consumiendo promesas

Una gran ventaja de emplear promesas es que podemos adjuntar tantos callbacks a una promesa como queramos, los cuales se ejecutarán una vez que la promesa se resuelva o rechace. Es decir, una promesa la pueden consumir varios consumidores.

Veamos un ejemplo completo. Supongamos que al entrar a una aplicación, queremos mostrar la información del usuario tanto en la barra de navegación como en el titulo de la página.

Si no empleasemos promesas, el código sería similar a:

var usuario = {
  perfilUsuario: null,

  obtenerPerfil: function() {
    if (!this.perfilUsuario) {
      var xhr = new XMLHttpRequest();
      xhr.open("GET","usuario.json", true);
      xhr.onreadystatechange = function() {
        if (xhr.readyState ===  4) {
          perfilUsuario = JSON.parse(xhr.responseText);
        }
      };
      xhr.send(null);
    }
  }
};

usuario.obtenerPerfil();
if (usuario.perfilUsuario) {  (1)
  document.getElementById("navbar").innerHTML = usuario.login;
  document.getElementById("titulo").innerHTML = usuario.nombre;
}
1 Pese a parecer que este código funciona, nunca se rellenarán los datos porque al ser una llamada asíncrona, la variable perfilUsuario estará a null.

Para que funcione correctamente, el código de rellenar la página se tiene que acoplar con la petición AJAX:

var usuario = {
  perfilUsuario: null,

  obtenerPerfil: function() {
    if (!this.perfilUsuario) {
      var xhr = new XMLHttpRequest();
      xhr.open("GET","usuario.json", true);
      xhr.onreadystatechange = function() {
        if (xhr.readyState ===  4) {
          perfilUsuario = JSON.parse(xhr.responseText);
          document.getElementById("navbar").innerHTML = perfilUsuario.login; (1)
          document.getElementById("titulo").innerHTML = perfilUsuario.nombre;
        }
      };
      xhr.send(null);
    }
  }
};

usuario.obtenerPerfil();
1 Ahora pintamos los datos tras recibirlos de manera asíncrona
Prometiendo AJAX

En cambio, si al obtener los datos AJAX, los envolvemos en una promesa, el código queda más legible y ahora sí que podemos desacoplar la petición AJAX del dibujado HTML:

var usuario = {
  promesaUsuario: null,

  obtenerPerfil: function() {
    if (!this.promesaUsuario) {
      this.promesaUsuario = new Promise(function(resolver, rechazar) {
        var xhr = new XMLHttpRequest();
        xhr.open("GET","usuario.json", true);
        xhr.onreadystatechange = function() {
          if (xhr.readyState ===  4) {
            resolver(JSON.parse(xhr.responseText));
          }
        };
        xhr.onerror = function() {
          rechazar(Error("Error al obtener usuario"));
        };
        xhr.send(null);
      });
    }
    return this.promesaUsuario;
  }
};

var navbar = {
  mostrar: function(usuario) {
    usuario.obtenerPerfil().then(function(perfil) {
      document.getElementById("navbar").innerHTML = perfil.login;
    });
  }
}

var titulo = {
  mostrar: function(usuario) {
    usuario.obtenerPerfil().then(function(perfil) {
      document.getElementById("titulo").innerHTML = perfil.nombre;
    });
  }
}

navbar.mostrar(usuario); (1)
titulo.mostrar(usuario);
1 Da igual si se ha obtenido el usuario. Como se obtiene una promesa, cuando se resuelva será cuando se dibujen los datos

Así pues, podemos añadir tanto consumidores a una misma promesa conforme necesitamos, los cuales se ejecutarán una vez de resuelva la misma.

Además, podemos añadir más callback cuando queramos, incluso si la promesa ya ha sido resuelta o rechazada (en dicho caso, se ejecutarán inmediatamente).

Devolviendo promesas

¿Y si queremos realizar una acción tras mostrar los datos del usuario? Al tratarse de una tarea asíncrona, necesitamos a su vez, que las operaciones de mostrar los datos también devuelvan una promesa.

Cualquier función que utilice una promesa debería devolver una nueva promesa

Ahora nuestro código quedará así:

// ...
var titulo = {
  mostrar: function(usuario) {
    return usuario.obtenerPerfil().then(function(perfil) {  (1)
      document.getElementById("titulo").innerHTML = perfil.nombre;
    });
  }
}
1 Como then devuelve una promesa, a su vez, hacemos que mostrar la propague

8.2.5. Prometiendo XMLHttpRequest

A día de hoy XMLHttpRequest no devuelve una promesa. Siendo, probablemente, la operación asíncrona más empleada por su uso en AJAX, el hecho de que devuelva una promesa va a simplicar las peticiones AJAX. Para ello, podemos crear una función que devuelve una promesa del siguiente modo:

function ajaxUA(url) {
  return new Promise(function(resolver, rechazar) {	(1)
    var req = new XMLHttpRequest();
    req.open('GET', url);

    req.onload = function() {
      if (req.status == 200) {
        resolver(req.response);	(2)
      } else {
        rechazar(Error(req.statusText));	(3)
      }
    };

    req.onerror = function() {	(4)
      reject(Error("Error de Red"));
    };

    req.send();
  });
}
1 Devolvemos una promesa
2 Si la petición ha ido bien, resolvemos la promesa
3 En cambio, si ha fallado, la rechazamos
4 Controlamos los posibles errores de red

Hecho esto, podemos realizar las peticiones del siguiente modo:

Ejemplo then
ajaxUA('heroes.json').then(function(response) {
  console.log("¡Bien!", response);
}, function(error) {
  console.error("¡Mal!", error);
});

Gracias al encadenamiento de promesas, podemos obtener el texto de una petición AJAX del siguiente modo:

ajaxUA('heroes.json').then(function(response) { (1)
  return JSON.parse(response);
}).then(function(response) {  (2)
  console.log("JSON de Heroes:", response);
});
1 Recibe la respuesta JSON
2 Recibe el objeto JavaScript parseado

Además, como JSON.parse recibe un único argumento y devuelve el texto transformado, podemos simplicarlo del siguiente modo:

ajaxUA('heroes.json').then(JSON.parse).then(function(response) {
  console.log("JSON de Heroes:", response);
});

8.2.6. Encadenando promesas

Acabamos de ver que tanto then como catch devuelven una promesa, lo que facilita su encadenamiento, aunque no devuelven una referencia a la misma promesa. Cada vez que se llama a uno de estos métodos se crea una nueva promesa y se devuelve, siendo estas dos promesas diferentes:

var p1,p2;

p1 = Promise.resolve();
p2 = p1.then(function() {
  // ....
});
console.log(p1 !== p2);  // true

Gracias a esto podemos encadenar promesas con el resultado de un paso anterior:

paso1().then(
  function paso2(resultadoPaso1) {
    // Acciones paso 2
  }
).then(
  function paso3(resultadoPaso2) {
    // Acciones paso 3
  }
).then(
  function paso4(resultadoPaso3) {
    // Acciones paso 3
  }
)

Cada llamada a then devuelve una nueva promesa que podemos emplear para adjuntarla a otro callback. Sea cual sea el valor que devuelve el callback resuelve la nueva promesa. Mediante este patrón podemos hacer que cada paso envíe su valor devuelto al siguiente paso.

Si un paso devuelve una promesa en vez de un valor, el siguiente paso recibe el valor empleado para completar la promesa. Veamos este caso mediante un ejemplo:

Ejemplo Encadenado de Promesas: http://jsbin.com/pibahakawi/edit?js,console
Promise.resolve('Hola!').then(
  function paso2(resultado) {
    console.log('Recibido paso 2: ' + resultado);
    return 'Saludos desde el paso 2';               // Devolvemos un valor
  }
).then(
  function paso3(resultado) {
    console.log('Recibido paso 3: ' + resultado);     // No devolvemos nada
  }
).then(
  function paso4(resultado) {
    console.log('Recibido paso 4: ' + resultado);
    return Promise.resolve('Valor completado');    // Devuelve una promesa
  }
).then(
  function paso5(resultado) {
    console.log('Recibido paso 5: ' + resultado);
  }
);

// Consola
// "Recibido paso 2: Hola!"
// "Recibido paso 3: Saludos desde el paso 2"
// "Recibido paso 4: undefined"
// "Recibido paso 5: Valor completado"

Al devolver un valor de manera explícita como en el paso dos, la promesa se resuelve. Como el paso 3 no devuelve ningún valor, la promesa se completa con undefined. Y el valor devuelto por el paso 4 completa la promesa que envuelve el paso 4.

8.2.7. Orden de ejecución de callbacks

Los promesas permiten gestionar el orden en el que se ejecuta el codigo respecto a otras tareas. Hay que distinguir bien entre qué elementos se ejecutan de manera síncrona, y cuales asíncronos.

La función resolver que se le pasa al constructor de Promise se ejecuta de manera síncrona. En cambio, todos los callbacks que se le pasan a then y a catch se invocan de manera asíncrona.

Veamoslo con un ejemplo:

Ejemplo orden de ejecución
var promesa = new Promise(function (resolver, rechazar) {
  console.log("Antes de la funcion resolver");
  resolver();
});

promesa.then(function() {
  console.log("Dentro del callback de completado");
});

console.log("Fin de la cita");

// Consola
// Antes de la funcion resolver
// Fin de la cita
// Dentro del callback de completado

8.2.8. Gestión de errores

Los rechazos y los errores se propagan a través de la cadena de promesas. Cuando se rechaza una promesa, todas las siguientes promesas de la cadena se rechazan como si fuera un dominó. Para detener el efecto dominó, debemos capturar el rechazo.

Para ello, además de utilizar el método then y pasar como segundo argumento la función para gestionar los errores, el API nos ofrece el método catch:

Ejemplo catch
ajaxUA('heroes.json').then(function(response) {
  console.log("¡Bien!", response);
}).catch(function(error) {
  console.error("¡Mal!", error);
});
En la práctica, se utiliza un método catch al final de la cadena para manejar todos los rechazos.

Veamos como utilizar un catch al final de una cadena de promesas:

Ejemplo catch al final de una cadena de promesas
Promise.reject(Error("Algo ha ido mal")).then(  (1)
  function paso2() {  (2)
    console.log("Por aquí no pasaré");
  }
).then(
  function paso3() {
    console.log("Y por aquí tampoco");
  }
).catch(  (3)
  function (err) {
    console.error("Algo ha fallado por el camino");
    console.error(err);
  }
);

// Consola
// Algo ha fallado por el camino
// Error: Algo ha ido mal
1 Creamos una promesa rechazada
2 No se ejectuan ni el paso 2, ni el 3, ya que la promesa no se ha cumplido
3 En cambio, si lo hace el manejador del error.
Excepciones y promesas

Una promesa se rechaza si se hace de manera explicita mediante la función rechazar del constructor, con Promise.reject o si el callback pasado a then lanza un error:

Vamos a repetir el ejemplo, pero lanzando un Error en el constructor de la promesa:

Ejemplo catch con Error lanzado por el constructor
rechazarCon("¡Malas noticias!").then(
  function paso2() {
    console.log("Por aquí no pasaré")
  }
).catch(
  function (err) {
    console.error("Y vuelve a fallar");
    console.error(err);
  }
);

function rechazarCon(cadena) {
  return new Promise(function (resolver, rechazar) {
    throw Error(cadena);
    resolver("No se utiliza");
  });
}
// Consola
// Y vuelve a fallar
// Error: ¡Malas noticias!

Este comportamiento resalta la ventaja de realizar todo el trabajo relacionado con la promesa dentro del callback del constructor, para que los errores se capturen automáticamente y se conviertan en rechazos.

Al lanzar el objeto Error para rechazar una promesa, la pila de llamadas se captura, lo que facilita el manejo del error en el manejador catch.
Autoevaluación

A partir del siguiente código anterior, ¿Qué mensajes saldrán por pantalla? [2]

var promesaJSON = new Promise(function(resolver, rechazar) {
  resolver(JSON.parse("Esto no es JSON"));
});

promesaJSON.then(function(datos) {
  console.log("¡Bien!", datos);
}).catch(function(err) {
  console.error("¡Mal!", err);
});
catch vs then(undefined, función)

El método catch es similar a emplear then(undefined, función), pero más legible. Por lo tanto, el anterior fragmento (Ejemplo catch) es equivalente a este:

Ejemplo then(undefined, función)
ajaxUA('heroes.json').then(function(response) {
  console.log("¡Bien!", response);
}).then(undefined, function(error) {
  console.error("¡Mal!", error);
});

Pero que sean similares no significan que sean iguales. Hay una sutil diferencia entre encadenar dos funciones then a hacerlo con una sola. Al rechazar una promesa se pasa al siguiente then con un callback de rechazo (o catch, ya que son equivalentes). Es decir, con then(func1, func2), se llamará a func1 o a func2, nunca a las dos. Pero con then(func1).catch(func2), se llamarán a ambas si func1 rechaza la promesa, ya que son pasos separados de la cadena.

Veamoslo con otro ejemplo:

paso1().then(function() {
  return paso2();
}).then(function() {
  return paso3();
}).catch(function(err) {
  return recuperacion1();
}).then(function() {
    return paso4();
  }, function(err) {
    return recuperacion2();
}).catch(function(err) {
  console.error("No me importa nada");
}).then(function() {
  console.log("¡Finiquitado!");
});

Este flujo de llamadas es muy similar a emplear try/catch en el lenguaje estandard de manera que los errores que suceden dentro de un try saltan inmediatamanete al bloque catch.

8.2.9. Promesas en paralelo

Si tenemos un conjunto de promesas que queremos ejecutar, y lo hacemos mediante un bucle, se ejecutarán en paralelo, en un orden indeterminado finalizando cada una conforme al tiempo necesario por cada tarea.

Supongamos que tenemos una aplicación bancaria en la cual tenemos varias cuentas, de manera que cuando el usuario entra en la aplicación, se le muestra el saldo de cada una de ellas:

Ejemplo promesas en paralelo
var cuentas = ["/banco1/12345678", "/banco2/13572468", "/banco3/87654321"];

cuentas.forEach(function(cuenta) {
  ajaxUA(cuenta).then(function(balance) {
    console.log(cuenta + " Balance -> " + balance);
  });
});

// Consola
// Banco 1 Balance -> 234
// Banco 3 Balance -> 1546
// Banco 2 Balance -> 789

Si además queremos mostrar un mensaje cuando se hayan consultado todas las cuentas, necesitamos consolidar todas las promesas en una nueva. Para ello, mediante el método Promise.all, podemos escribir código de finalización de tareas paralelas, del tipo "Cuando todas estas cosas hayan finalizado, haz esta otra".

El comportamiento de Promise.all(arrayDePromesas).then(function(arrayDeResultados) es el siguiente: devuelve una nueva promesa que se cumplirá cuando lo hayan hecho todas las promesas recibidas. Si alguna se rechaza, la nueva promesa también se rechazará. El resultado es un array de resultados que siguen el mismo orden de las promesas recibidas.

Así pues, reescribimos el ejemplo y tendremos:

Ejemplo promesas en paralelo, finalización única
var cuentas = ["/banco1/12345678", "/banco2/13572468", "/banco3/87654321"];

var peticiones = cuentas.map(function(cuenta) {
  return ajaxUA(cuenta);
});

Promise.all(peticiones).then(function (balances) {
  console.log("Los " + balances.length + " han sido actualizados");
}).catch(function(err) {
  console.error("Error al recuperar los balances", err);
})
// Consola
// Los 3 balances han sido actualizados

8.2.10. Promesas en secuencia

Cuando hemos estudiado como encadenar promesas, hemos visto como podemos codificar que una promesa se ejecute cuando ha finalizado la anterior. Para ello, de antemano tenemos que saber las promesas que vamos a gestionar.

¿Y si el número de promesas es indeterminado? Es decir, si tenemos un array con las promesas que se tienen que ejecutar secuancialmente ¿Cómo puedo hacerlo?

Una solución es hacerlo mediante un bucle iterativo:

Ejemplo promesas en secuencia, mediante reduce
function secuencia(array, callback) {
  var seq = Promise.resolve();

  array.forEach(function (elem) {
    seq = seq.then(function() { (1)
      return callback(elem);
    });
  });
}

secuencia(cuentas, function (cuenta) {
  return ajaxUA(cuenta).then(function(balance) {
    console.log(cuenta + " Balance -> " + balance);
  });
})
1 en cada iteración, seq contiene el valor de la secuencia anterior hasta que se resuelve y almacena la nueva promesa

Si empleamos la función reduce, el código se reduce al ahorrarnos la variable seq:

Ejemplo promesas en secuencia, mediante reduce
function secuencia(array, callback) {
  return array.reduce(function cadena(promesa, elem) {  (1)
    return promesa.then(function() {
      return callback(elem);
    });
  }, Promise.resolve());  (2)
}
1 La variable promesa almacena el valor anterior. La función cadena siempre devuelve una promesa que resuelve el callback.
2 El array se reduce hasta finalizarlo y devolver la última promesa de la cadena

En cambio, mediante código recursivo, pese a que pensemos que podemos desbordar la pila, cada llamada se coloca encima de la pila, de manera que el bucle de eventos la resuelve en primer lugar:

Ejemplo promesas en secuencia, mediante recursión
function secuencia(array, callback) {
  function cadena(array, indice) {
    if (indice === array.length) {
      return Promise.resolve();
    } else {
      return Promise.resolve(callback(array[indice])).then(function() {
        return cadena(array, indice + 1);
      });
    }
  }
  return cadena(array, 0);
}

Aunque ambos planteamientos obtengan el mismo resultado, los dos enfoques tienen sus diferencias. Mientras que mediante el bucle se contruye la cadena entera de promesas sin esperar a que se resuelvan, mediante el planteamiento recursivo, las promesas se añaden bajo demanda tras resolver la promesa anterior. Así pues, el enfoque recursivo permite decidir si continuar o romper la cadena en base al resultado de una promesa anterior.

Las librerías de terceros que permiten gestionar las promesas ofrecen funciones auxiliares para trabajar con promesas en secuencia o de manera paralela.

8.2.11. Carrera de promesas

En ocasiones vamos a realizar diferentes peticiones que realizan la misma acción a diferentes servicios, pero sólo nos interesará el que nos devuelve el resultado más rápidamente.

Para ello, el API ofrece el método Promise.race(arrayDePromesas), la cual reduce el array de promesas y devuelve una nueva promesa con el primer valor disponible. Es decir, se examina cada promesa hasta que una de ellas finaliza, ya sea resuelta o rechazada, la cual se devuelve.

Mediante esta operación, podemos crear un mecanismo para gestionar la latencia de una petición a servidor, de manera que si tarda más de X segundos, acceda a una caché. En el caso de que falle la cache, el mecanismo fallará.

Ejemplo de carrera de promesas
function obtenerDatos(url) {
  var tiempo = 500;  // ms
  var caduca = Date.now() + tiempo;

  var datosServer = ajaxUA(url);

  var datosCache = buscarEnCache(url).then(function (datos) {
    return new Promise(function (resolver, rechazar) {
      var lapso = Math.max(caduca: Date.now(), 0);
      setTimeout(function () {
        resolver(datos);
      }, lapso);
    })
  });

  var fallo = new Promise(function (resolver, rechazar) {
    setTimeout(function () {
      rechazar(new Error("No se ha podido recuperar los datos de " + url));
    }, tiempo);
  });

  return Promse.race([datosServer, datosCache, fallo]);
}

Se trata de una solución simplificada, donde se ha omitido el código de buscarEnCache, y no se han planteado todos los escenarios posibles.

Autoevaluación

¿Qué pasaría se la petición a los datos del servidor falla inmediatamente debido a un fallo de red? [3]

Librerías de programación reactiva como RxJS, Bacon.js y Kefir.js están diseñadas especificamente para estos escenarios

8.3. Fetch API

Aprovechando las promesas, ES6 ofrece el Fetch API para realizar peticiones AJAX que directamente devuelvan un promesa. Es decir, ya no se emplea el objeto XMLHttpRequest, el cual no se creó con AJAX en mente, sino una serie de métodos y objetos diseñados para tal fin. Al emplear promesas, el código es más sencillo y limpio, evitando los callbacks anidados.

Actualmente, el API lo soportan tanto Google Chrome como Mozilla Firefox. Podéis comprobar el resto de navegadores en http://caniuse.com/#search=fetch

El objeto window ofrece el método fetch, con un primer argumento con la URL de la petición, y un segundo opcional con un objeto literal que permite configurar la petición:

Ejemplo Fetch API
// url (obligatorio), opciones (opcional)
fetch('/ruta/url', {
	method: 'get'
}).then(function(respuesta) {

}).catch(function(err) {
	// Error :(
});

8.3.1. Hola Fetch

A modo de ejemplo, vamos a reescibir el ejemplo de la sesión 5 que hacíamos uso de AJAX, pero ahora con la Fetch API. De esta manera, el código queda mucho más concreto:

fetch('http://www.omdbapi.com/?s=batman', {
    method: 'get'
}).then(function(respuesta) {
  if (!respuesta.ok) {
    throw Error(respuesta.statusText);
  }
  return respuesta.json();
}).then(function(datos) {
    var pelis = datos.Search;
    for (var numPeli in pelis) {
        console.log(pelis[numPeli].Title + ": " + pelis[numPeli].Year);
    }
}).catch(function(err) {
	console.error("Error en Fetch de películas de Batman", err);
});

Como podemos observar, haciendo uso de las promesas, podemos encadenarlas y en el último paso comprobar los errores.

Por ejemplo, podemos refactorizar el control de estado y extraelo a una función:

function estado(respuesta) {
  if (respuesta.ok) {
    return Promise.resolve(respuesta);
  } else {
    return Promise.reject(new Error(respuesta.statusText));
  }
}

De este modo, nuestro código quedaría así:

fetch('http://www.omdbapi.com/?s=batman', {
  method: 'get'
}).then(estado)
.then(function (respuesta) {
  return respuesta.json();
}).then(function(datos) {
  var pelis = datos.Search;
  for (var numPeli in pelis) {
    console.log(pelis[numPeli].Title + ": " + pelis[numPeli].Year);
  }
}).catch(function(err) {
  console.error("Error en Fetch de películas de Batman", err);
});

8.3.2. Cabeceras

Una ventaja de este API es la posibilidad de asignar las cabeceras de las peticiones mediante el objeto Headers(), el cual tiene una estructura similar a una mapa. A continuación se muestra un ejemplo de como acceder y manipular las cabeceras mediante los métodos append, has, get, set y delete:

var headers = new Headers();

headers.append('Content-Type', 'text/plain');
headers.append('Mi-Cabecera-Personalizada', 'cualquierValor');

headers.has('Content-Type'); // true
headers.get('Content-Type'); // "text/plain"
headers.set('Content-Type', 'application/json');

headers.delete('Mi-Cabecera-Personalizada');

// Add initial values
var headers = new Headers({ (1)
  'Content-Type': 'text/plain',
  'Mi-Cabecera-Personalizada': 'cualquierValor'
});

Para usar las cabeceras, las pasaremos como parámetro de creación de una petición mediante una instancia de Request:

var peticion = new Request('/url-peticion', {
  headers: new Headers({
    'Content-Type': 'text/plain'
  })
});

fetch(peticion).then(function() { /* manejar la respuesta */ });

8.3.3. Petición

Para poder configurar toda la información que representa una petición se emplea el objeto Request. Este objeto puede contener las siguientes propiedades:

  • method: GET, POST, PUT, DELETE, HEAD

  • url: URL de la petición

  • headers: objeto Headers con las cabeceras asociadas

  • body: datos a enviar con la petición

  • referrer: referrer de la petición

  • mode: cors, no-cors, same-origin

  • credentials: indica si se envian cookies con la petición: include, omit, same-origin

  • redirect: follow, error, manual

  • integrity: valor integridad del subrecurso

  • cache: tipo de cache (default, reload, no-cache)

Con estos parámetros, una petición puede quedar así:

var request = new Request('/heroes.json', {
  method: 'get',
  mode: 'cors',
  headers: new Headers({
    'Content-Type': 'text/plain'
  })
});

fetch(request).then(function() { /* manejar la respuesta */ });

Realmente, el método fetch recibe una URL a la que se envía una petición, y un objeto literal con la configuración de la petición, por lo que el código queda mejor así:

fetch('/heroes.json', {
  method: 'GET',
  mode: 'cors',
  headers: new Headers({
    'Content-Type': 'text/plain'
  })
}).then(function() { /* manejar la respuesta */ });
Enviando datos

Ya sabemos que un caso muy común es enviar la información de un formulario vía AJAX.

Para ello, además de configurar que la petición sea POST, le asociaremos en la propiedad body un FormData creado a partir del identificador del elemento del formulario:

fetch('/submit', {
  method: 'post',
  body: new FormData(document.getElementById('formulario-cliente'))
});

Si lo que queremos es enviar JSON al servidor, en la misma propiedad, le asociamos un nuevo objeto JSON:

fetch('/submit-json', {
  method: 'post',
  body: JSON.stringify({
    email: document.getElementById('email').value
    comentarios: document.getElementById('comentarios').value
  })
});

O si queremos enviar información en formato URL:

fetch('/submit-urlencoded', {
  method: 'post',
  headers: {
    "Content-type": "application/x-www-form-urlencoded; charset=UTF-8"
  },
  body: 'heroe=Batman&nombre=Bruce+Wayne'
});

8.3.4. Respuesta

Una vez realizada la petición, dentro del manejador, reciberemos un objeto Response, compuesto de una serie de propiedades que representan la respuesta del servidor, de las cuales extraremos la información:

  • type: indican el origen de la petición. Dependiendo del tipo, podremos consultar diferente información:

    • basic: proviene del mismo origen, sin restricciones.

    • cors: acceso permitido a origen externo. Las cabeceras que se pueden consultar son Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, y Pragma

    • opaque: origen externo que no devuelve cabeceras CORS, con lo que no podemos leer los datos ni visualizar el estado de la petición.

  • status: código de estado (200, 404, etc.)

  • ok: Booleano que indica si la respuesta fue exitosa (en el rango 200-299 de estado)

  • statusText: código de estado (OK)

  • headers: objeto Headers asociado a la respuesta.

Además, el objeto Response ofrece los siguientes métodos para crear nuevas respuestas con diferente código de estado o a un destino distino:

  • clone(): clona el objeto Response

  • error(): devuelve un nuevo objeto Response asociado con un error de red.

  • redirect(): crea una nueva respuesta con una URL diferente

Finalmente, para transformar la respuesta a una promesa con un tipo de dato del cual extraer la información de la petición, tenemos los siguientes métodos:

  • arrayBuffer(): Devuelve una promesa que se resuelve con un ArrayBuffer.

  • blob(): Devuelve una promesa que se resuelve con un Blob.

  • formData(): Devuelve una promesa que se resuelve con un objeto FormData.

  • json(): Devuelve una promesa que se resuelve con un objeto JSON.

  • text(): Devuelve una promesa que se resuelve con un texto (USVString).

Veamos estos métodos en funcionamiento.

Parseando la respuesta

A día de hoy, el estandar es emplear el formato JSON para las respuestas. En vez de utilizar JSON.parse(cadena) para transformar la cadena de respuesta en un objeto, podemos utilizar el método json():

fetch('heroes.json').then(function(response) {
  return response.json();
}).then(function(datos) {
  console.log(datos); // datos es un objeto JavaScript
});

Si la información, viene en texto plano o como documento HTML, podemos emplear text():

fetch('/siguientePagina').then(function(response) {
  return response.text();
}).then(function(texto) {
  // <!DOCTYPE ....
  console.log(texto);
});

Finalmente, si recibimos una imagen o archivo en binario, hemos de emplear blob():

fetch('paisaje.jpg').then(function(response) {
  return response.blob();
})
.then(function(blobImagen) {
  document.querySelector('img').src = URL.createObjectURL(blobImagen);
});

8.4. jQuery Deferreds

Los Deferreds son la implementación que hace jQuery de las promesas, ofreciendo una implementación de las promesas independiente de la versión de ECMAScript que tenga nuestro navegador.

8.4.1. $.Deferred()

Cada promesa de jQuery comienza con un Deferred. Un Deferred es solo una promesa con métodos que permiten a su propietario resolverla o rechazarla. Todas las otras promesas son de sólo lectura.

Para crear un Deferred, usaremos el constructor $.Deferred(). Una vez creada una promesa podemos invocar los siguientes métodos:

  • state(): devuelve el estado

  • resolve(): resuelve la promesa

  • reject(): rechaza la promesa

var deferred = new $.Deferred();

deferred.state();  (1)
deferred.resolve();	(2)
deferred.state();  (3)
deferred.reject(); (4)
1 Devuelve el estado de la promesa. Nada más creada su estado es pending.
2 Le indicamos a la promesa que ha finalizado la acción que queremos controlar.
3 Volvemos a obtener el estado, pero ahora ya es resolved.
4 Permite rechazar la promesa. En este ejemplo no tiene efecto, ya que la promesa se ha resuelto.
Mucho cuidado con diseñar una aplicación que se base en comprobar el estado de una promesa para actuar en consecuencia. El único momento que tiene sentido actuar dependiendo de su valor es cuando tras un callback de done() querramos averiguar si hemos llegado tras resolver o rechazar la promesa.

Si al método constructor le pasamos una función, ésta se ejecutará tan pronto como el objete se cree, y la función recibe como parámetro el nuevo objeto deferred. Mediante esta característica podemos crear un envoltorio que realiza una tarea asíncrona y que dispare un callback cuando haya finalizado:

function realizarTarea() {
	return $.Deferred(function(def) {
		// tarea async que dispara un callback al acabar
	});
}

Podemos obtener una promesa "pura" a partir del método promise(). El resultado es similar a Deferred, excepto que faltan los métodos de resolve() y reject().

var deferred = new $.Deferred();
var promise = deferred.promise();

promise.state();  // "pending"
deferred.reject();
promise.state();  // "rejected"

El método promise() existe principalmente para dar soporte a la encapsulación: si una función devuelve un Deferred, puede ser resuelta o rechazada por el programa que la invoca. En cambio, si sólo devolvemos la promesa pura correspondiente al Deferred, el programa que la invoca sólo puede leer su estado y añadir callbacks. La propia jQuery sigue este enfoque al devolver promesas puras desde sus métodos AJAX:

var obteniendoProductos = $.get("/products");

obteniendoProductos.state();  // "pending"
obteniendoProductos.resolve;  // undefined
Las promesas se suelen nombrar con gerundios para indicar que representan un proceso

8.4.2. Manejadores de promesas

Una vez tenemos una promesa, podemos adjuntarle tantos callbacks como queremos mediante los métodos:

  • done(): se lanza cuando la promesa se resuelve. correctamente, mediante resolve()

  • fail(): se lanza cuando la promesa se rechaza, mediante reject()

  • always(): se lanza cuando se completa la promesa, independientemente que su estado sea resuelta o rechazada

Por ejemplo:

var promesa = $.Deferred();
promesa.done(function() {
  console.log("Se ejecutará cuando la promesa se resuelva.");
});

promesa.fail(function() {
  console.log("Se ejecutará cuando la promesa se rechace.");
});

promesa.always(function() {
  console.log("Se ejecutará en cualquier caso.");
});

Por ejemplo, supongamos que tenemos tres botones que modifican la promesa:

<button id="btnResuelve">Resolver</button>
<button id="btnRechaza">Rechazar</button>
<button id="btnEstado">¿Estado?</button>

Y si capturamos los eventos click para que modifiquen la promesa, tendremos:

$("#btnResuelve").click(function() {
  promesa.resolve();
});
$("#btnRechaza").click(function() {
  promesa.reject();
});
$("#btnEstado").click(function() {
  console.log(promesa.state());
});

8.4.3. Encadenando promesas

Los métodos de los objetos Deferred se pueden encadenar, de manera que cada método devuelve a su vez un objeto Deferred donde poder volver a llamar a otros métodos:

promesa.done(function() {
  console.log("Se ejecutará cuando la promesa se resuelva.");
}).fail(function() {
  console.log("Se ejecutará cuando la promesa se rechace.");
}).always(function() {
  console.log("Se ejecutará en cualquier caso.");
});

Mediante el método then() podemos agrupar los tres callbacks de una sola vez:

promesa.then(doneCallback, failCallback, alwaysCallback);

Con lo que si reescribimos el ejemplo tendríamos:

promesa.then(function() {
  console.log("Se ejecutará cuando la promesa se resuelva.");
}, function() {
  console.log("Se ejecutará cuando la promesa se rechace.");
}, function() {
  console.log("Se ejecutará en cualquier caso.");
});

Además, el orden en el que se adjuntan los callbacks definen su orden de ejecución. Por ejemplo, si duplicamos los callbacks y volvemos a añadirlos, tendremos:

Supongamos que reescribimos el ejemplo anterior con el siguiente fragmento para manejar las promesas:

var promesa = $.Deferred();
promesa.done(function() {
  console.log("Primer callback.");
}).done(function() {
  console.log("Segundo callback.");
}).done(function() {
  console.log("Tercer callback.");
});

promesa.fail(function() {
  console.log("Houston! Tenemos un problema");
});

promesa.always(function() {
  console.log("Dentro del always");
}).done(function() {
  console.log("Y un cuarto callback si todo ha ido bien");
});

Si probamos a resolver la promesa, la salida por consola será la siguiente:

"Primer callback."
"Segundo callback."
"Tercer callback."
"Dentro del always"
"Y un cuarto callback si todo ha ido bien"

8.4.4. Modelando con promesas

Veamos en detalle un caso de uso de promesas para representar una serie de acciones que puede realizar el usuario a partir de un formulario básico con AJAX que contiene un campo de texto con observaciones.

Queremos asegurarnos que el formularios solo se envia una vez y que el usuario recibe una confirmación cuando envía las observaciones. Además, queremos separar el código que describe el comportamiento de la aplicación del código que trabaja el contenido de la página. Esto facilita el testing y minimiza la cantidad de código necesario que necesitaríamos modificar si cambiamo la disposición de la página.

Así pues, separamos la lógica de aplicación mediante promesas:

var enviandoObservaciones = new $.Deferred();

enviandoObservaciones.done(function(input) {
  $.post("/observaciones", input);
});

Y la interacción DOM se encarga de modificar el estado de la promesa:

$("#observaciones").submit(function() {
  enviandoObservaciones.resolve($("textarea", this).val());	(1)

  return false;  (2)
});

enviandoObservaciones.done(function() {
  $("#contenido").append("<p>¡Gracias por las observaciones!</p>");	(3)
});
1 Cuando el usuario pulsa sobre enviar, se resuelve la promesa. Si le pasamos un parámetro a un método de promesa (ya sea ), el parámetro se reenvía al callback correspondiente.
2 Prevenimos el comportamiento por defecto del navegador.
3 Le añadimos un segundo manejador que nos permite separar el código de aplicación del tratamiento del DOM.

8.4.5. Prestando promesas del futuro

Si queremos mejorar nuestro formulario, más que asumir de manera optimista que nuestro POST funcionará correctamente, deberíamos primero indicar que el formulario se ha enviado, por ejemplo, mediante una barra de progreso, y posteriormente indicarle al usuario si el envío ha sido exitoso o fallido cuando el servidor responda.

Para ello, podemos adjuntar callbacks a la promesa que devuelve $.post. El problema viene cuando tenemos que manipular el DOM desde esos callbacks, y hemos planteado usar promesas para separar el código de aplicación del de manipulación del DOM.

Una manera de separar la creación de una promesa del callback de lógica de aplicación es reenviar los eventos de resolve/reject desde la promesa POST a una promesa que se encuentre fuera de nuestro alcance. En vez de necesitar varias líneas con código anidado del tipo promesa1.done(promesa2.resolve);.., lo haremos mediante then() (antes de jQuery 1.8 mediante pipe()).

Para ello, then() devuelve una nueva promesa que permite filtrar el estado y los valores de una promesa mediante una función, lo que la convierte en una ventana al futuro, permitiendo adjuntar comportamiento a una promesa que todavía no existe.

Si ahora mejoramos el código del formulario haciendo que nuestra promesa POST se encole en una nueva promesa llamada guardandoObservaciones, tendremos que el código de aplicación quede así:

var enviandoObservaciones = new $.Deferred();
var guardandoObservaciones = enviandoObservaciones.pipe(function(input) {
  return $.post("/observaciones", input);	(1)
});
1 Encolamos la promesa de guardado tras el envío

Con lo que la manipulación del DOM queda del siguiente modo:

$("#observaciones").submit(function() {
  enviandoObservaciones.resolve($("textarea", this).val());
  return false;
});

enviandoObservaciones.done(function() {
  $("#contenido").append("<div class='spinner'>");	(1)
});

guardandoObservaciones.then(
  function() {	// done
    $("#contenido").append("<p>¡Gracias por las observaciones!</p>");
  }, function() {	// fail
    $("#contenido").append("<p>Se ha producido un error al contactar con el servidor.</p>");
  }, function() {	// always
    $("#contenido").remove(".spinner");	(2)
});
1 Al enviar los datos creamos la barra de progreso
2 Cuando finaliza el guardado, quitamos la barra de progreso
Intersección de promesas

Parte de la magia de las promesas es su naturaleza binaria. Como solo tienen dos estados, se pueden combinar igual que si fuesen booleanos (aunque todavía no sepamos que valores tendrán).

De la misma manera que tenemos Promise.all, jQuery ofrece el método $.when() como operador equivalente a la intersección (Y).

Así pues, dada una lista de promesas, when() devuelve una nueva promesa como resultado de otras promesas y que cumple estas reglas:

  • Cuando todas las promesas recibidas se resuelven, la nueva promesa estará resuelta.

  • Cuando alguna de las promesas recibidas se rechaza, la nueva promesa se rechaza.

Por ello, cuando estemos esperando que múltiples eventos desordenados ocurran y necesitamos realizar una acción cuando todos hayan finalizado deberemos utilizar when(). Por ejemplo, el caso de uso más común es al realizar varias llamadas AJAX simultáneas:

$("#contenido").append("<div class='spinner'>");
$.when(	$.get("/datosEncriptados"),$.get("/claveEncriptacion"))
  .then(function() {	// done
    // ambas llamadas han funcionado
  }, function() {	// fail
    // una de las llamadas ha fallado
  }, function() {	// always
    $("#contenido").remove(".spinner");
});

Si las dos promesas se resuelven, entonces la promesa agregada también se resuelve, y se llamará la función del primer callback (done()). Sin embargo, en cuanto una de las promesas se rechace, la promesa agregada se rechazará también, disparando el callback de fail().

Otro caso de uso es permitir al usuario solicitar un recurso que puede o no estar disponible. Por ejemplo, supongamos un widget de un chat que cargamos con YepNope:

var cargandoChat = new $.Deferred();
yepnope({
  load: "recursos/chat.js",
  complete: cargandoChat.resolve
});

var lanzandoChat = new $.Deferred();
$("#lanzarChat").click(lanzandoChat.resolve);
lanzandoChat.done(function() {
  $("#contenidoChat").append("<div class='spinner'>");
});

$.when(cargandoChat, lanzandoChat).done(function() {
  $("#contenidoChat").remove(".spinner");
  // comienza el chat
});

8.4.6. Deferreds y AJAX

Las promesas facilitan mucho el trabajo con AJAX y el trabajo con acceso simultáneos a recursos remotos.

El objeto jxXHR que se obtiene de los métodos AJAX como $.ajax() o $.getJSON() implementan el interfaz Promise, por lo que vamos a poder utilizar los métodos done, fail, then, always y when.

Así pues, vamos a poder escribir código similar al siguiente, donde tras hacer la petición vamos a poder utilizar los callbacks que ofrecen las promesas:

function getDatos() {
  var peticion = $.getJSON("http://www.omdbapi.com/?s=batman&callback=?");
  peticion.done(todoOk).fail(function() {
    console.log("Algo ha fallado");
  });
  peticion.always(function() {
    console.log("Final, bien o mal");
  });
}

function todoOk(datos) {
  console.log("Datos recibidos y adjuntándolos a resultado");
  $("#resultado").append(JSON.stringify(datos));
}

En resumen, el uso de promesas simplifica la gestión de eventos asíncronos evitando múltiples condiciones anidadas. Ya veremos en el módulo de Angular como dicho framework hace un uso extenso de las promesas.

8.5. Ejercicios

8.5.1. (0.5 ptos) Ejercicio 81. Star Wars Fetch

A partir del ejercicio 72 sobre Star Wars API, vamos a reescribir el contenido mediante Fetch API y el uso de promesas para reducir el código y simplificar la lógica de las llamadas.

En este caso, el listado de películas se tiene que pintar en el DOM de una sola vez, no conforme se reciben los títulos de las películas con cada petición AJAX.

Todo el código estará incluido en el archivo ej81.js.

8.5.2. (0.3 ptos) Ejercicio 82. Star Wars Deferreds

A partir del ejercicio 72 sobre Star Wars API, se pide reescribir el contenido para hacer uso de Deferreds.

De igual manera que el ejercicio anterior, el listado de películas se tiene que pintar en el DOM de una sola vez, no conforme se reciben los títulos de las películas con cada petición AJAX.

Todo el código estará incluido en el archivo ej82.js.


1. Sólo el valor de PI, ya que una vez resuelta, la promesa no puede tomar otro valor
2. ¡Mal! y el error de parseo de JSON inválido, ya que se lanza una excepción en el constructor y por tanto no se resuelve la promesa
3. La promesa datosServer se rechazaría y por consiguiente no se comprobaría la caché