Promesas en Javascript

29 de abril 2019ComentariosjavascriptDavid Poza SuárezComentarios

Las promesas son una de las novedades más importantes en ECMAScript 6, que permite realizar operaciones de forma asíncrona, es decir, sin bloquear el dibujado de la pagina, como son por ejemplo las llamadas a una API. Todo ello lo consigue a pesar de que Javascript es single threaded, es decir que solo tenemos un hilo para hacer todas las operaciones de nuestra aplicación, tanto dibujado como cálculos.

En otro artículo hablaremos de los Web Workers de Html5, que permiten usar múltiples hilos para ejecutar porciones de código y por tanto tener concurrencia real, eso si, siendo una instancia totalmente separada del hilo principal.

gracias a la asincronía podemos construir interfaces que no se congelan mientras se espera a que un proceso finalice, por ejemplo un proceso en el servidor, que no sabemos cuándo acabará.

gracias a la asincronía podemos construir interfaces que no se congelan mientras se espera a que un proceso finalice, por ejemplo un proceso en el servidor, que no sabemos cuándo acabará.

Es muy importante saber usarlas ya que, no solo mejora la estructura de nuestro código, sino que nos permite entender muchas utilidades que ya las incorporan (como el cliente para mongodb, axios o el propio fetch también de ES6 . No obstante el concepto de promesa no es nuevo y ya existían librerías para implementarlas, como por ejemplo: q o bluebird.

Se definen como un objeto que representa un valor que estará disponible en un futuro indefinido, o tal vez nunca. Es decir, puede cumplirse o no, como en la vida real.

Por lo tanto puede tener 3 estados:

  • Pendiente: Su estado inicial, hasta que se haya devuelto un valor mediante la ejecución de resolve() o un error mediante reject().
  • Resuelta: Cuando se ejecuta el método resolve().
  • Rechazada: Cuando se ejecuta el método reject().
  • Se dice que ha finalizado cuando alcanza cualquiera de los dos estados anteriores, y a partir del este momento se puede encadenar con otra promesa.

Algo muy similar a las promesas son los clásicos callbacks, que también permitían realizar tareas de forma asíncrona. Se trata de funciones pasadas como parámetro que se ejecutan cuando finaliza la función que las recibe.

Un ejemplo real de uso de callbacks podría ser un típico ajax con jquery. Vamos a hacer login contra una api para obtener el token de autenticación, luego el id de usuario, y a continuación una lista de fechas con tareas para finalmente construir el objeto resultado: un array de tantos objetos como fechas y cada una de ellos con la una propiedad date y otra tasks con las tareas que tengamos asignadas para ese día.

Aviso: Es un ejemplo que yo siempre trataría de resolver desde el lado del servidor, preferiblemente elaborando una consulta a base de datos usando join y group by, sin embargo aquí se trata de ver como se trabaja con promesas. Y puede ser que en la vida real también demos con casos donde no tenemos acceso al desarrollo de la api y las opciones que nos brinda no se adapten a nuestras necesidades.

let base\_url = "https://dpstogglapi1.davidinformatico.com/\_/";
   $.ajax({
    url: base\_url+'auth/authenticate',
    type: "POST",
    data:{
      "email" : 'demo@davidinformatico.com',
      "password" : "demo"
    },
    dataType: "json",
    success: (data,status)=>{
      let token = data.data.token;
      $.ajax({
        url: base\_url+'users/me?fields=\*',
        type: "GET",
        headers: {
          "Accept": "application/json",
          "Content-Type": "application/json",
          "Authorization": "Bearer " + token
        },
        dataType: "json",
        success: (data,status)=>{
          let user\_id = data.data.id;
          $.ajax({
            url: base\_url+'items/tasks?fields=date&filter\[user\]\[eq\]='+user\_id+'&groups=date&sort=-date',
            type: "GET",
            headers: {
              "Accept": "application/json",
              "Content-Type": "application/json",
              "Authorization": "Bearer " + token
            },
            dataType: "json",
            success: (data,status)=>{
              let resultado = data.data.map(d=>{
                $.ajax({
                  url: base\_url+'items/tasks?fields=\*&filter\[user\]\[eq\]='+user\_id+'&filter\[date\]\[eq\]='+d.date,
                  type: "GET",
                  headers: {
                    "Accept": "application/json",
                    "Content-Type": "application/json",
                    "Authorization": "Bearer " + token
                  },
                  dataType: "json",
                  success: (data,status)=>{
                    d.tasks = data.data;
                  },
                  error: ()=>{
                    console.log(data);
                  }
                });
                return d;
              });
              console.log(resultado);
            },
            error: ()=>{
              console.log(data);
            }
          });
        },
        error: ()=>{
          console.log(data);
        }
      });
    },
    error: (data)=>{
      console.log(data);
    }
  });

Como podemos ver, con tan solo 4 niveles ya apreciamos el denominado Callback Hell, que provoca que nuestro código se vaya indentando rápidamente, quedando en forma de pirámide, y por tanto muy poco legible.

Veamos ahora el mismo ejemplo igual de simple pero usando encadenamiento de fetch, que usa promesas, pues devuelve un objeto tipo Promise.:

token = "";
user\_id = "";
let base\_url = "https://dpstogglapi1.davidinformatico.com/\_/";
fetch(base\_url+'auth/authenticate', {
 method: "POST",
 headers: {
   "Accept": "application/json",
   "Content-Type": "application/json"
 },
 body: JSON.stringify({
   email: "demo@davidinformatico.com",
   password: "demo"
 })
})
.then(
  (response)=>response.json()
)
.then(
  (data) => {
   if(data.error) throw new Error(data.error.message);
   token = data.data.token;
   return fetch(base\_url+'users/me?fields=\*', {
     method: "GET",
     headers: {
       "Accept": "application/json",
       "Content-Type": "application/json",
       "Authorization": "Bearer " + token
     }
   })
  }
)
.then(
  (response)=>response.json()
)
.then(
 (data) => {
   user\_id = data.data.id;
   if(data.error) throw new Error(data.error.message);
   return fetch(base\_url+'items/tasks?fields=date&filter\[user\]\[eq\]='+user\_id+'&groups=date&sort=-date', {
     method: "GET",
     headers: {
       "Accept": "application/json",
       "Content-Type": "application/json",
       "Authorization": "Bearer " + token
     }
   })
 }
)
.then(
  (response)=>response.json()
)
.then(
 (data) => {
   if(data.error) throw new Error(data.error.message);
   return data.data.map(d=>(
     fetch(base\_url+'items/tasks?fields=\*&filter\[user\]\[eq\]='+user\_id+'&filter\[date\]\[eq\]='+d.date, {
       method: "GET",
       headers: {
         "Accept": "application/json",
         "Content-Type": "application/json",
         "Authorization": "Bearer " + token
       }
     })
   ));
 }
) //en el anterior then se devuelve un array de promesas
.then( //queremos continuar solo cuando se cumplan todas ellas, para ello usamos Promise.all()
  (promises)=>Promise.all(promises)
)
.then(
  (responses)=>responses.map(r=>r.json())
)
.then(
  (promises)=>Promise.all(promises)
)
.then(
  (data)=>data.map(d=>{
    if(d.error) throw new Error(d.error.message);
    return(d.data);
  })
)
.then(
  (data)=>console.log(data)
)
.catch(
  (error)=>console.log(error)
)

En ambos ejemplos, tanto el que usa callbacks como el que usa fetch obtenemos la misma respuesta:

El objeto resultante de la cadena de consultas

El objeto resultante de la cadena de consultas

Promise

Ahora que hemos visto un ejemplo de fetch, que devuelve objetos Promise, vamos a ver cómo funciona éste, ya que podríamos querer construir una función similar a fetch, que tenga que esperar a una posible resolución o error.

En el siguiente ejemplo vamos a crear nuestra propia Promise, que va a resolverse o fallar de forma aleatoria por medio de una llamada a Math.random. Podemos continuar con la ejecución del programa dejando la promesa pendiente, y la atenderemos cuando se resuelva (mediante el bloque de código .then) o cuando falle, (mediante el bloque de código catch). Recibiendo ambos bloques una función como parámetro, que a su vez recibe el valor emitido por el método resolve() o reject().

De este modo cada vez que recarguemos la pagina veremos el mensaje de inicio y 2 segundos más tarde el mensaje de Éxito o el mensaje de Error.

let promesa = new Promise((resolver, rechazar) =>{
  setTimeout(()=>{
    let numero = Math.random();
    if(numero >= 0.5) resolver("Éxito");
    rechazar("Error");
  }, 2000);
});
promesa.then(
    (data) => console.log(data)
)
.catch(
    (data) => console.log(data)
)
console.log("inicio del proceso");

Promise.all

En el ejemplo de fetch podemos ver otro método interesante de promise, se trata de .all(), que recibe una lista o array de objetos Promise, y que devuelve una promesa cumplida en caso de todas las promesas se cumplan o una promesa rechazada en cuanto una sola de ellas sea rechazada.

Nos servirá para ejecutar código cuando éste necesite la garantía de haber recibido toda una serie de resultados que podrían fallar o tener diferentes tiempos de ejecución.

Catch

Por último es muy interesante la facilidad y limpieza que nos aportan las promesas para capturar errores. Para ello se usan los bloques catch, que se pueden encadenar con los bloques then. Podemos hacerlo usando un solo catch al final del todo o bien usando un catch después de cada then.

Es decir, podemos querer controlar un error de forma genérica, de la misma forma sea cual sea la promesa de la cadena que nos falle, o bien podemos querer controlarlo de forma particular para cada promesa. Siendo por tanto muy versátil.

aquí vemos el flujo de una cadena de promesas, donde podemos capturar errores en medio o al final del todo

aquí vemos el flujo de una cadena de promesas, donde podemos capturar errores en medio o al final del todo

Throw

Esta sentencia no es de uso exclusivo en promesas, pero si que nos permite emitir objetos de tipo Error, que van a ser capturados por los bloques catch. Algo que es muy útil por ejemplo en el uso de fetch. Pues fetch por sí misma podría no fallar salvo que se pierda la conexión, la url no exista, etc., pero el contenido de la consulta podría devolver un error. Por ejemplo si el usuario y contraseña son erróneos, no tiene sentido continuar con el resto de consultas y habría que parar la cadena de promesas. Para ello comprobamos el contenido de la consulta recibida la propiedad error.message (así se ha programado la api del ejemplo en cuestión) y lanzaríamos un objeto Error mediante throw.

Por lo tanto, aunque no quise mencionar las promesas en el artículo de novedades de ecmascript 6, esto fue a propósito para dedicarle un artículo, ya que es sin duda una de los cambios que más vamos a ver y con lo que hay que familiarizarse cuanto antes.

Como nota al artículo: ahora estoy desarrollando una aplicación full-stack usando React en el frontend y con una api en express (nodejs), basándome en una base de datos no relacional mongodb. Por ello en las próximas semanas escribiré artículos sobre el lado del servidor, y en ellos vamos a poder ver como el cliente de mongodb para javascript, aunque puede funcionar con callbacks, también incorpora la opción de trabajar con promesas, algo que ya no nos resultará extraño.