Un repaso a las promesas de ES6

En mi post anterior sobre decoradores en ES7 (es2016) comenté mi intención de escribir un artículo sobre las funciones asíncronas pero creo que para poder entender como trabajan este tipo de funciones primero es necesario comprender como funcionan las promesas, los iteradores y los generadores de ES6. En este artículo daremos un repaso a las promesas (Promise) de ES6.

 

un-repaso-a-las-promesas-en-es6

 

Si ya has trabajado con librerías como Q, Bluebird o RSVP verás que la API de las promesas de ES6, aunque algo más escasa, es bastante similar. En cualquier caso empecemos por el principio por si alguien aún no sabe de que va esto de las promesas.

Las promesas representan una operación que no se ha completado pero que se espera se complete en un futuro, son como ‘un proxy para un valor que podrá estar disponible’.

Aunque se pueden utilizar tanto en operaciones síncronas como en operaciones asíncronas, su potencia reside en la facilidad y claridad que aportan a la hora de trabajar con operaciones asíncronas, además de aportar una solución bastante elegante al infierno de los callbacks o ‘callback hell’, tema del que se ha hablado mucho en la comunidad JavaScript.

 

Creando una promesa

El método estándar de crear promesas es mediante el uso de new Promise, el cual solo requiere de un argumento, una función que se ejecutará inmediatamente después de invocarse al constructor del objeto Promise. El propósito de esta función es informar al objeto Promise que la promesa se ha resuelto (fulfilled) o bien se ha rechazado (rejected).

let promise = new Promise((resolve, reject) => { 
  // do something
  if (error) {
    reject(reason)  // failure
  } else {
    resolve(value)  // success
  }
})

El objeto Promise cuenta con varios estados:

  • pending: estado inicial, sin haberse cumplido o rechazado.
  • fulfilled: la operación se ha completado con éxito.
  • rejected: algo salió mal. La operación ha fallado.

Poco que comentar a cerca de los estados, creo que su propio nombre ya nos da bastantes pistas. Una vez que la promesa se ha cumplido o rechazado el estado quedará permanentemente asociado a ella y no se podrá modificar, lo que significa que una promesa puede tener éxito o fracasar una sola vez.

 

La API

Como he comentado antes la API es algo más escasa que la que nos ofrecen algunas librerías pero en general cubre las necesidades más comunes.

ciclo-de-una-promesa

 

En el diagrama de arriba se puede observar como funciona una promesa y que métodos se desencadenan. Como podréis ver la ‘chicha’ está en los métodos del prototipo .then.catch así que empecemos por ellos.

 

.then(onFulfilled, onRejected)

Este método es el encargado de registrar la devolución de la llamada para recibir o bien el valor devuelto por la promesa o bien la razón por la que no puede cumplirse.

promise.then(result => {
    console.log(result)
  }, error => {
    console.error(error)   // something bad happened’
})

.catch(onRejected)

Se trata de un método específico para controlar que la promesa ha sido rechazada y toma como argumento el motivo por el cual la promesa se ha rechazado. Su función es similar a la del segundo argumento de .then.

promise.then(result => {
  console.log(result)
}).catch(error => {
  console.log(error)
})

Como vemos en este caso, si la promesa se rechaza .then no tiene como comprobarlo, este control pasará ahora a .catch.

Existen otros métodos bastante interesantes del constructor del objeto Promise. Vamos a repasarlos por encima para no alargarnos mucho.

Promise.all(iterable)

Devuelve una promesa que se resolverá en el momento que todas las promesas del argumento hayan sido resueltas.

Promise.all([
  fetch('foo'),
  fetch('bar')
]).then(responses => responses.map(response => response.statusText))
  .then(status => console.log(status.join(', ')))

Promise.race(iterable)

Devuelve una promesa que se resuelve o se rechaza en en momento en que una de las promesas se resuelve o se rechaza y devuelve el valor de la promesa si se ha cumplido o el motivo si se ha rechazado.

Promise.race([
  fetch('foo'),
  fetch('bar')
]).then(response => console.log(response.statusText))

Promise.reject(reason)

Devuelve un objeto Promise con el motivo del rechazo.

Promise.reject(reason)

Promise.resolve(value)

Al igual que reject devuelve un objeto Promise pero en este caso con el valor dado.

Promise.resolve('foo')

 

Consumiendo promesas

Llegados a este punto imagino que ya hay ganas de ver como podemos consumir promesas y aplicar lo expuesto hasta ajora así que veamos algunos ejemplos. Comenzaremos por uno bastante sencillo que básicamente implementa un setTimeout basado en promesas. La idea para el ejemplo está sacada del blog de Axel Rauschmayer, si aún no lo conoces te recomiendo que lo visites regularmente.

const timeout = ms => { 
  return new Promise((resolve, reject) => {
    setTimeout(resolve(ms), ms)
  })
}

timeout(3000).then(ms => {
  console.log(`you lost ${ms} milliseconds`)  // you lost 3000 milliseconds :(
})

Bien, pasados tres segundos nuestra promesa se resuelve, se ejecuta el .then y la consola nos ‘pinta’ el mensaje con el valor devuelto por resolve. Hasta aquí ningún misterio, todo se comporta como esperábamos y ya que en este caso sería bastante extraño que la promesa se rechazase no necesitamos controlar este caso.

Ahora veamos otro ejemplo un poco más práctico y más completo en el que ‘algo’ podría fallar y ser rechazada la promesa. Vamos a crear un método que lea un fichero y nos lo devuelva. Por hacer algo diferente vamos a usar el método fs.readFile del objeto FileSystem de Node en lugar del típico request.

const readFile = filepath => {
  return new Promise((resolve, reject) => {
    fs.readFile(filepath, (error, data) => {
      if (error) reject(error)
      else resolve(data)
    })
  })
}

let file = readFile('path/to/file')
    .then(data => data)
    .catch(error => { throw error })

En este caso tenemos una condición para controlar el estado de nuestra promesa. Como buena práctica es aconsejable comprobar el error en primer lugar, si este ocurriese ejecutaremos reject, de lo contrario resolveremos la promesa.

 

Encadenando .then

Como punto interesante que habréis observado en el diagrama de la API es que los .then se pueden encadenar, –una operación llamada composición-. Esto es posible porque estos métodos a su vez también devuelven promesas. Supongamos que en el ejemplo anterior estamos pidiendo un archivo .md y necesitásemos convertirlo a texto, podríamos hacer lo siguiente:

import marked from 'marked'

let file = readFile('file.md')
    .then(md => marked(md))
    .then(txt => console.log(txt))
    .catch(error => { throw error })

También es posible encadenar los .catch, aunque hasta ahora no se me ha dado el caso en el que necesitar hacerlo. Si se da o se me ocurre un caso práctico actualizaré el post.

Hasta aquí el repaso a las promesas en ES6. Creo que queda claro el potencial que tienen para escribir código asíncrono de una manera elegante y fácil y evitar problemas en el código anidando callbacks.

Comentar que las promesas ya van siendo soportadas por las versiones más modernas de los navegadores, en cualquier caso os dejo este enlace a la tabla de compatibilidades.

Hasta la próxima!