Por amor al código Code snippets y reflexiones sobre tecnología

Promesas en Javascript

El objeto Promise es ya un standard de ES2015 y puede utilizarse en la mayoría de los navegadores. Para poder utilizarlo y seguir soportando IE (esa carga que todos los desarrolladores web llevamos a cuestas) existe un polyfill que apenas ocupa 1kb.

En esos enlaces puedes encontrar lo que es una Promesa y cómo funciona, pero las especificaciones no son precisamente fáciles de leer. Personalmente encuentro mucho más útil aprender con metáforas y ejemplos que muestren cómo se utiliza un concepto concreto.

¿Qué es una promesa y cómo se utiliza?

Decimos que una promesa es como un recibo que nos dan de un pedido. Sabemos que va a tardar X, y que no lo tenemos justo al pagar, pero el recibo y su localizador nos aseguran que tendremos el producto en el futuro.

Imagen de un ticket de una cola de espera

Poniéndonos algo más técnicos, una promesa es un objeto que encapsula una operación asíncrona. La operación asíncrona (una llamada AJAX, un evento, una llamada a una función programada para el futuro) tiene una duración indeterminada, pero al crear una promesa obtenemos inmediatamente un objeto con el que podemos trabajar. Es decir:


var promise = new Promise(function(resolve, reject) {
  
  function sayHello() {
    resolve('Hello World!')
  }

  setTimeout(sayHello, 10000)

})

console.log(promise)
// Promise { ... }
// promise es un objeto, no tenemos que esperar para poder usarlo!

En este punto la variable promise es un objeto y podemos operar con él. Hasta dentro de 10 segundos no se establecerá su valor y no contendrá la cadena ‘Hello world!’ pero podemos seguir trabajando con ella como si si que fuera así.

.then es un método de la promesa que acepta una función, cuando la promesa se resuelve, llama a la función que le pasamos a .then pasándole como parámetro lo que le pasáramos a resolve. En realidad, le estamos pasando a .then la función resolve. Veamos como se haría en el ejemplo anterior:


promise.then(function(message) {
  console.log(message) 
})

// 'Hello World!'

Así dicho parece que lo que estamos haciendo es complicar las cosas, pero las promesas tienen dos ventajas principales:

  1. Nos devuelven el control. Gracias a las promesas, mantenemos control sobre la ejecución de nuestro programa, que antes delegábamos en una llamada asíncrona que podía o no terminar. Ahora con las promesas podemos operar independientemente de lo que pase con la llamada asíncrona. Además, las promesas tienen la gran ventaja de que si el código lanza una excepción dentro de la Promesa, ésta la capturará y la devolverá convenientemente con la función ‘catch’.

  2. Las promesas se pueden encadenar. Podemos hacer que la ejecución de una promesa dependa de otra, o esperar a que todo un grupo de promesas se resuelvan. Esto hace que el código sea mucho menos engorroso y mucho más fácil de leer y mantener.

Vamos a ver varios ejemplos prácticos y su implementación para aclarar conceptos.

El ejemplo típico de utilización de una promesa es una llamada AJAX. Si has usado jQuery, la sintaxis es muy similar, y de hecho en jQuery 3.0 han modificado el código para que se comporte como una Promesa, ya que antes había algunas diferencias.


$.get('http://...')
  .done(function(data) {
    // operaciones con data
  })
  .fail(function(error) {
    // muestra el error
  })

Pero si no necesitamos jQuery para nada más, a lo mejor no queremos incluirl en nuestro proyecto sólo para esto. Por suerte, los navegadores ya comienzan a soportar la función ‘fetch’, que devuelve una promesa y funciona de forma parecida a la función ajax de jQuery, pero usando la terminología estandard:


fetch('http://...')
  .then(function(response) {
    // operaciones con response
  })
  .catch(function(error) {
    // muestra el error
  })

Estas funciones sólo son válidas para peticiones AJAX, pero las promesas no se restringen solo a esto. Podemos usarlas para cualquier operación no síncrona. Podemos usarlas para funciones que queremos ejecutar en el futuro como en el primer ejemplo, o para encapsular la ejecución de eventos, controlar procesos que tardan cierto tiempo en ejecutarse, peticiones a la cache, etc.

Un ejemplo diferente de cómo utilizar una promesa es utilizarlas para ejecutar nuestra aplicación cuando el DOM se ha cargado y está listo.


function ready() {
  
  return new Promise(function(resolve, reject) {
    
    // Resuelve la promesa cuando el DOM está listo
    document.addEventListener('readystatechange', function() {
      if(document.readyState !== 'loading') {
        resolve()
      }
    })

  })

}

ready().then(function() {
  // Aquí podemos hacer cosas con el DOM
})

Ese es un ejemplo de un evento que sabemos que ocurrirá solo una vez y que podemos capturar en una promesa. Y lo de solo una vez es importante porque una vez la promesa toma un valor, no se modificará. Se dice que la promesa se ha cumplido, resuelto o establecido.

Además de ‘resolverse’ las promesas pueden ‘denegarse’. Para denegarlas, llamaremos a la segunda función que recibimos como parámetro que normalmente recibe el nombre de ‘reject’:


var promise = new Promise(function(resolve, reject) {
  
  function sayHello() {
    resolve('Hello World!')
  }

  reject('Something went wrong!!')

  setTimeout(sayHello, 10000)

})

promise
  .then(function(message) {
    console.log(message)
  })
  .catch(function(err) {
    console.error('ERROR: ' + err)
  })

// 'ERROR: Something went wrong!'

Cuando una promesa es rechazada, la ejecución saltará directamente al primer .catch que encuentre, saltándose el/los then (luego veremos que puede hacer varios) que haya delante.

Pero como una promesa una vez tome un valor no se modificará, si se resuelve y algo más tarde tratamos de denegarla, la promesa ignorará la llamada a ‘reject’ y mantendrá el valor con el que se resolvió. Esto hace que podamos añadir un tiempo de expiración a nuestras promesas. Por ejemplo, para una petición o un evento que esperamos que se ejecute antes de un tiempo determinado, podríamos añadir un tiempo máximo por el que esperar a que se descargue una imagen o un script y si se supera ese tiempo, tomarlo como un error:


function ready(element) {
  
  return new Promise(function(resolve, reject) {
    
    element.addEventListener('onload', function() {
      resolve()
    })

    element.addEventListener('onerror', function(err) {
      reject('There was an error')
    })

    // timeout en 1s
    setTimeout(function(){
      reject('Something must be wrong, try again or fallback to something else')
    }, 1000)

  })
}

ready(image)
  .then(function() {
    // Imagen cargada correctamente
  })
  .catch(function() {
    // Hubo un error, mostrar mensaje o cargar imagen por defecto...
  })

Lo importante en este ejemplo es que hemos encapsulado todo el manejo de los eventos del elemento dentro de la promesa, y de cara al exterior ahora solo tenemos una función que devuelve una promesa, lo que simplifica enormente el código y mejora su reusabilidad y legibilidad.

En el ejemplo anterior estamos denegando la promesa ante errores o expiración del tiempo de espera. Pero en lugar de denegar la promesa podríamos lanzar una excepción y el resultado sería el mismo: la promesa capturaría la excepción y se denegaría. Esto puede usarse con funciones que puedan causar excepciones, como lectura de ficheros o de base de datos. Hay un buen ejemplo de esto en la documentación de Bluebird:


fs.readFileAsync("file.json")
  .then(JSON.parse)
  .then(function (val) {
    console.log(val.success)
  })
  .catch(SyntaxError, function (e) {
    console.error("invalid json in file")
  })
  .catch(function (e) {
    console.error("unable to read file")
  })

Otra de las grandes ventajas de las promesas que aún no hemos probado, es la de encadenarlas. Decimos que las promesas son ‘thenables’, es decir que se les puede poner un ‘then’ detrás y pasarán el resultado con el que se han resuelto a la función que le pasemos al then. De esta forma podemos hacer que la ejecución de una promesa dependa del resultado de otra, sin necesidad de anidarlas:


fetch('http://search')
  .then(function getFirstVideo(result1) {
    // Hacer cosas con results
    return fetch('http://...')
  })
  .then(function showVideoData(result2) {
    // result2 depende de result1 pero la estructura del código es lineal :)
  })

Esto también ayuda a simplificar el código y sobre todo a librarnos de la terrible pirámide de callbacks que se genera al tener que anidar las llamadas asíncronas en javascript para poder acceder al valor de la llamada anterior.

También podemos hacer que una promesa dependa de que se terminen varias promesas que se ejecutaron paralelamente:


Promise.all([promise1, promise2, promise3])
  .then(function(arrayOfResults) {

  })

La función all, además de devolver todos los resultados en orden independientemente de cuándo se resuelvan las promesas, fallará si alguna de ellas falla, con lo que nuestro código sólo se ejecutará si tenemos los resultados de todas las promesas.

.all ejecuta todas las promesas de forma paralela, no en secuencia, es decir, es para ejecutar un montón de promesas que no dependen de las demás para ejecutarse, como por ejemplo descargar información meteorológica de varias ciudades a la vez para compararlas.

Además de .all, las promesas de ES6 incluyen la función .race. Race ejecuta un array de promesas como .all pero el then se ejecutará en el momento en el que la primera promesa se resuelva, sin esperar a las demás. Como su nombre indica, .race es un una carrera para quedarnos con la promesa que resuelva más rápido. Esto nos puede servir para consultar varios servicios a la vez y quedarnos con el primer resultado que llegue. En el ejemplo del tiempo, podríamos consultar el tiempo para la misma cuidad en diferentes APIs y devolver al usuario el primer resultado, obteniendo así el resultado más rápido posible.


var p1 = new Promise(function(resolve, reject) { 
    setTimeout(resolve, 500, "one")
})

var p2 = new Promise(function(resolve, reject) { 
    setTimeout(resolve, 100, "two")
})

Promise.race([p1, p2]).then(function(value) {
  console.log(value) 
})

// "two"
// Los dos se resuelven, pero p2 es más rápida

Estas funciones pueden crearse gracias a que, como comentaba antes, las Promesas encapsulan operaciones asíncronas devolviéndonos siempre el mismo interface, dándonos una forma fácil de manipularlas y agruparlas.

Tanto es así, que, con algunas modificaciones, las promesas podrían ser una mónada, en este hilo de github se puede seguir una interesante conversación entre varios desarrolladores muy conocidos sobre ese tema. Cuando las promesas estaban en proceso de especificación en el lenguaje, algunos programadores del mundo de la programación funcional se quejaron de que no eran “correctas”, no permitían las operaciones de composición que podrían permitir y así los desarrolladores de JavaScript perdíamos un gran potencial. Recientemente André Stalz ha renovado el debate en su blog

Aun así, la solución de las promesas de Javascript nos da un interface que es cási una mónada y sobre el que también podemos operar, aunque no sea de forma tan genérica, con operaciones como map/filter/reduce, esto es lo que hace Bluebird, dándonos todo el repertorio de operaciones que podemos hacer con promesas, lo que resulta muy útil cuando todas nuestras librerías devuelven promesas y podemos manejarlas a alto nivel. Además, si nuestra librería no está escrita con promesas, sino con el estilo de callbacks de node, pero queremos aprovechar las ventajas de las promesas, Bluebird nos da una función para convertir las funciones que usan callbacks a promesas: promisify

ACTUALIZACIÓN: Node también incluye una función promisify desde la versión 8, con lo que ya no es necesario usar Bluebird para esto.

Patrones útiles

Cuando llevas un tiempo usando promesas de forma regular, te das cuenta de que hay ciertos patrones de código que se repiten.

Composición/flujo de promesas

Una de las coas que ocurre cuando usas promesas es que acabas teniendo que usarlas para todo. Si no pierdes las ventajas de la captura de errores y el encapsulamiento. Esto es debido al problema que hemos comentado antes de que las promesas son muy dogmáticas en su comportamiento y te obligan a moldear tu código para utilizarlas. Todo esto para decir que al usar promesas es habitual tener código que depende de una ejecución secuencial de promesas:


promesa1()
  .then(res => promesa2(res))
  .then(res => promesa3(res))
  .then(res => promesa4(res))
  .then(res => promesa5(res))

Es algo que suele ocurrir cuando tenemos que preparar algún tipo de entorno, como un entorno de testing o la inicialización de la app o el provisioning de una base de datos y necesitamos ejecutar una serie de acciones de manera secuencial. Puedes ver un ejemplo con código real en este repositorio de couchbase:


// Run the example
verifyNodejsVersion()
  .then(storeInitial)
  .then(lookupEntireDocument)
  .then(subdocItemLookupTwoFields)
  .then(subdocArrayAdd)
  .then(lookupEntireDocument)
  .then(subdocArrayManipulation)
  .then(subDocumentItemLookup)
  .then(subdocArrayRemoveItem)
  .then(subDocumentItemLookup)
  .then(() => {
    process.exit(0);
  })
  .catch((err) => {
    console.log("ERR:", err)
    process.exit(0);
  });

En este caso, el código ejecuta una serie de acciones sobre la base de datos y sale, como un proceso por lotes, aunque es solo un ejemplo de uso.

Seguramente, ver ese ejemplo hace que algo no te parezca bien. Aunque encadenar las funciones de esta forma es lineal y muy funcional y nos permite tener control sobre las excepciones, este código no es DRY, hay algo que se repite en casi todas las líneas: .then(. Es ver algo así y a la mayoría se nos pone la piel de gallina, hay algo que no está bien en tener que repetirse tanto. Y tenemos razón, lo que ocurre es que el .then tiene un comportamiento muy peculiar que hace que tengamos que usarlo así y no podamos componerlo directamente. Lo ideal, lo que está pidiendo este código, sería poder hacer:


pipe(
  verifyNodejsVersion,
  storeInitial,
  lookupEntireDocument,
  subdocItemLookupTwoFields,
  subdocArrayAdd,
  lookupEntireDocument,
  subdocArrayManipulation,
  subDocumentItemLookup,
  subdocArrayRemoveItem,
  subDocumentItemLookup,
  () => process.exit(0)
)
.catch((err) => {
  console.log("ERR:", err)
  process.exit(0);
});
// ERROR...

Si te estás preguntándo qué hace la función pipe, puedes verlo en este artículo sobre programación funcional Pero sigue leyendo y lo aclaro en un momento con la solución.

Pero no podemos hacer eso con las promesas. ¡pipe no llama a .then! Esa era la principal queja de los programadores funcionales cuando querían cambiar la especificación de las promesas de JavaScript. No podemos usar pipe, ni compose, ni map, ni fold, ni ninguna de todas esas herramientas funcionales tal y como están.

Pero, aunque esta no sea una situación ideal, y algunos hayan decidido hacer sus propias promesas que encajen dentro del resto de la programación funcional, lo que sí que podemos hacer es nuestra propia función pipePromises. Y además no es nada del otro mundo:


function pipePromises(...promisesFunctions) {
  return promisesFunctions.reduce(
    (lastPromise, f) => lastPromise.then(f), 
    Promise.resolve()
  )
}


Esta función encadena las promesas usando .then y pasando el resultado de una a la siguiente. Con esta función podríamos encadenar las promesas sin repetir los .then y manteniendo la funcionalidad del primer código:


pipePromises(
  verifyNodejsVersion,
  storeInitial,
  lookupEntireDocument,
  subdocItemLookupTwoFields,
  subdocArrayAdd,
  lookupEntireDocument,
  subdocArrayManipulation,
  subDocumentItemLookup,
  subdocArrayRemoveItem,
  subDocumentItemLookup,
  () => process.exit(0)
)
.catch((err) => {
  console.log("ERR:", err)
  process.exit(0);
});

Puedes comprobar que funciona con este fiddle:


function pipePromises(...promisesFunctions) {
  return promisesFunctions.reduce(
    (lastPromise, f) => lastPromise.then(f), 
    Promise.resolve()
  )
}

const promiseInc = i => Promise.resolve(i + 1)

pipePromises(
  () => promiseInc(0),
  promiseInc,
  promiseInc,
  promiseInc,
  promiseInc
)
.then(console.log)
// 5

El árbol de promesas

El problema más común con las promesas es el mismo que con los callbacks: en lugar de un “árbol de navidad” de callbacks podemos acabar creando un árbol de navidad de promesas.


promiseA()
  .then(resultA => {
      return functionB(resultA)
        .then(resultB => {
          return doSomethingFancy(resultA, resultB)
        })
    })

Esto ocurre porque la función doSomethingFancy necesita los valores devueltos por las dos promesas y functionB también necesita el valor de la primera promesa. Si estamos usando Bluebird, join puede ayudarnos con esto, pero si no queremos aprender otro API más o añadir otra dependencia a nuestro proyecto, hay varias formas de solucionar el problema con las promesas nativas de es6:

Utilizando variables en el ámbito superior.


function sample() {
  var resultA
  return promiseA()
    .then(res => resultA = res)
    .then(() => functionB(resultA))
    .then(resultB => doSomethingFancy(resultA, resultB))
}

Simplemente copiamos los resultados de las promesas en variables accesibles por todas las funciones por estar en un ámbito superior. El código vuelve a ser lineal y solo hemos tenido que añadir una variable, bastante limpio y fácil de leer, suele ser mi primera opción para este problema.

Otra solución utilizando ‘.all’:


promiseA()
  .then(resultA => Promise.all([resultA, functionB(resultA)]))
  .then(([resultA, resultB]) => {
    return doSomethingFancy(resultA, resultB)
  })

De esta forma no necesitamos crear una variable externa, la funcion ‘.all’ acepta valores y promesas y nos devuelve un array con todos los resultados. La sintaxis gracias a es6 es bastante legible aunque algo menos que la anterior.

Utilizando una función auxiliar:


function joinPromises(functionA, functionAB) {
  return (resultA) => {
    return functionB(resultA).then(resultB => functionAB(resultA, resultB))
  }
}

promiseA()
  .then(resultA => {
    return joinPromises(functionB, doSomethingFancy)(resultA)
  })

En este caso hemos creado una pequeña función que nos ayuda a pasar los parámetros, se podría generalizar para N argumentos, pero así queda más fácil de leer y se entiende mejor el ejemplo (ver el final del artículo para una generalización parecida). De hecho, utilizando la notación ‘pointFree’ podemos dejar el ejemplo incluso más conciso:


function joinPromises(functionA, functionAB) {
  return (resultA) => {
    return functionB(resultA).then(resultB => functionAB(resultA, resultB))
  }
}

promiseA().then(joinPromises(functionB, doSomethingFancy))

Anti-patrones al utilizar promesas

A la hora de usar promesas también hay que tener cuidado de no caer en algunas malas prácticas que harán que perdamos las ventajas de las promesas por el camino. Un ejemplo (fuente: taoofcode,ver algo más abajo) de un código que puede parecer correcto e incluso más legible según al tipo de código que estemos acostumbrado sería:


function anAsyncCall() {
  var promise = doSomethingAsync()
  promise.then(function() {
    somethingComplicated()
  })
  
  return promise
}

Parece que lo estamos haciendo bien, pero en realidad estamos devolviendo la primera promesa en lugar del resultado del then, con lo que las excepciones que puedan provocarse y el propio resultado del then se perderán, esto puede comprobarse con el siguiente ejemplo (jsfiddle):


doSomethingAsync = function() {
  return Promise.resolve(5)
}

somethingComplicated = function(num) {
  throw 'Error, too much complexity'
  return num * 2
}

function anAsyncCall() {
  var promise = doSomethingAsync()
  promise.then(function(res) {
    somethingComplicated(res)
  })
  
  return promise
}

anAsyncCall()
  .then(function(res) {
    console.log(res)
  })
  .catch(function(err) {
    console.error(err)
  })

El catch no captura la excepción, además, si no se produjera la excepción, el resultado de ‘somethingComplicated’ se pierde porque no lo estamos devolviendo en el then.

Veamos el ejemplo corregido (jsfiddle):


doSomethingAsync = function() {
  return Promise.resolve(5)
}

somethingComplicated = function(num) {
  throw 'Error, too much complexity'
  return num * 2
}

function anAsyncCall() {
  var promise = doSomethingAsync()
  return promise.then(function(res) {
    return somethingComplicated(res)
  });
}

anAsyncCall()
  .then(function(res) {
    console.log(res)
  })
  .catch(function(err) {
    console.error(err)
  })

Con el nuevo código sí se captura la excepción. Además, si comentamos la línea del throw, el resultado por consola es 10 en lugar de 5, porque estamos obteniendo el resultado de ejecutar ‘somethingComplicated’ lo que, seguramente, era la intención de escribir el código de esta manera.

Seguiré editando el artículo y añadiéndo ejemplos en cuanto pueda, pero mientras dejo un par de artículos sobre el tema: Bluebird - Anti-patterns, We have a problem with promises y Promises anti-patterns en taoofcode

Conclusiones

La conclusión a todo esto: utiliza promesas para mejorar la legibilidad y fiabilidad de tu código y recuperar el control de la ejecución del código asíncrono. Este es un artículo largo, pero solo he dado un repaso superficial a las propiedades y usos de las promesas. Es importante aprender a usarlas correctamente, pero son una herramienta muy poderosa para mejorar tu código en JavaScript.

Material para ampliar

Hay mucha documentación de calidad en Inglés y online sobre promesas, algunos enlaces de referencia son:

Curso sobre promesas en Udacity Muy buen material y ejercicios para fijar los conocimientos, los cursos de Udacity son de los mejores cursos online que he probado.

Javascript’s Promises: An Introduction Fantástica y extensa introducción a las promesas por Jake Archibald.

Capítulo You don’t know JS @getify tiene todo un capítulo de uno de sus libros dedicado a las promesas. Como siempre contenido en profundidad e impecable, estos libros son de obligada referencia para cualquier desarrollador de JavaScript.

Articulo en MDN La documentación de MDN sobre promesas, también en español.

Si quieres comentar, corregir algo o contribuir, puedes hacerlo en twitter o en Github. Cualquier comentario, crítica o contribución será bien recibido.