Funciones async en JavaScript

Pensá en serie, ejecutá en paralelo – Funciones async en JavaScript

El comité TC39, encargado de la estandarización de ECMAScript (JavaScript para los amigos), incluyó en la versión ES2016/ES7 las llamadas Async Functions. Esta nueva característica, proveniente de C#, permite estructurar nuestro código asíncrono utilizando sintaxis del mundo síncrono, facilitando la manera en que pensamos el flujo de la aplicación.

Utilizando el caso de los llamados XHR vamos a mostrar la evolución en el trato del código asíncrono en JavaScript para llegar a esta nueva construcción llamada «Async Functions».

En el comienzo todo era callback

Hace mucho tiempo, en una galaxia muy muy lejana, la gente de Microsoft inventó una tecnología que luego se haría conocer como AJAX. Como los requests al servidor son muy lentos y el navegador solo permitía el uso de un thread para nuestro código (el mismo thread de UI), realizar un request xhr y esperar por la respuesta para seguir con el flujo de la app no es una opción ya que bloquearía el uso del resto de la aplicación para el usuario. Es por eso que cada vez que ejecutábamos/ejecutamos un request utilizando el objeto XMLHttpRequest pasamos una función (o más) a ejecutar cuando la respuesta es recibida.

Usando jQuery para el ejemplo, un pedido AJAX al servidor se ve así:

function pedirPartidos(competencia, success, error) {
  $.ajax({
    "url": "/api/partidos.json",
    "success": success,
    "error": error,
    "data": {
      "competencia": competencia      
    }
  });
}

function copaAmerica() {
  pedirPartidos('Copa America', function(data) {
    if(!data.juegaMessi) mostrarError(new Error('No se transmite'));
  }, mostrarError);
}

Este approach, que es el más utilizado hasta la actualidad, tiene algunos problemas. En principio el flujo de la función tiene ramas donde el órden de ejecución no se condice con el órden del código dentro de la misma. El programa va a ejecutarse en este órden:

  • Request a /api/partidos.json
  • Termina la ejecución de copaAmerica y el programa sigue ejecutando otras tareas y escuchando eventos.
  • Retorna el llamado a la función:
    1. Si el request fue exitoso se ejecuta la primera función pasada por parámetro y si todo está bien, luego se llama a prepararLaTele.
    2. Si falló el request se ejecuta la segunda función de error.

Por otra parte la función copaAmerica no puede retornar el valor de la data pedida al servidor ya que esta termina su ejecución antes de que el request pueda ser completado.

Además, si otro request depende de la respuesta del primero, tengo que llamarlo desde la función de callback, facilitando lo que se conoce como «callback hell».

Callback hell

Zona de promesas

Una respuesta a esta falta de estructura es la introducción de un objeto llamado «Promise» o (Promesa), con su implementación estrella Promises/A+. La idea de las promesas es mitigar algunos de los problemas de usar callbacks para operaciones async:

  1. Retorna un objeto que «promete» ser completado
  2. Hace uso de excepciones

Volviendo a nuestro ejemplo de la Copa América, podemos reescribirlo utilizando promesas. (Los métodos de AJAX de jQuery retornan promesas desde la versión 1.5).

function pedirPartidos(competencia) {
  return $.getJSON("/api/partidos.json", { "competencia": competencia });
}

function copaAmerica() {
  var promesaPartidos = pedirPartidos('Copa America')
  .then(function(data) {
    if(!data.juegaMessi) throw new Error('No se transmite');
    prepararLaTele(data, 'HD'); // Salió bien
  })
  .catch(function(error) {
    mostrarError(error);
  });

  return promesaPartidos;
}

En este caso vemos como las funciones ahora sí retornan valores, pudiendo pasar promesas entre secciones del programa. La función que llame a copaAmerica va a poder hacer uso de promesaPartidos y a la vez no es necesario el pasaje de callbacks entre funciones.

Otro punto de interés tiene que ver con la capacidad de las promesas de emitir errores y handlearlos. Esta es una mejora circunstancial respecto del uso de callbacks donde no es posible.

Promises/A+ fue incluido en el estándar de ES2015 (ES6), es decir que ya está en la última versión terminada del lenguaje y ya se puede usar en algunos navegadores sin la necesidad de librerías o polyfills.

Async/Await al rescate

Si bien las promesas ayudan en gran parte al problema del flujo de código asíncrono, no resuelven completamente, el mindset sigue siendo parecido al del uso de callbacks. Para mejorar la experiencia a la hora de desarrollar se va a incluir en el próximo estándar llamado ES2016, la estructura llamada «Async Functions». Gracias al uso de Promises y la introducción de generadores en el lenguaje, sumado a 2 nuevas keywords «async» y «await» podemos repensar como escribimos código async.

Nuevamente vamos a reescribir el ejemplo, ahora utilizando una función async:

function pedirPartidos(competencia) {
  return $.getJSON("/api/partidos.json", { "competencia": competencia });
}

async function copaAmerica() {
  try {
    var partidos = await pedirPartidos('Copa America');
    if(partidos.juegaMessi) throw new Error('No se transmite');
     prepararLaTele(data, 'HD'); // Salió bien
     return partidos;
  } catch(e) {
    mostrarError();
  }
}

Ahora sí, nuestro código tiene estructura de código síncrono y el valor de retorno de la función es simplemente la información que necesitamos y que puede ser recogida por otra función async. La estructura try/catch (de ES3) nos permite handlear errores sin necesidad de uso de estructuras ad-hoc.

¿Qué pasa si ahora quiero pedirle al servidor información de 2 competencias en paralelo y devolver cuando estén completas?

async function copaAmerica() {
  var partidos = await*([pedirPartidos('Copa América'), messi('Argentino B')]);
  console.log(partidos); // [{//data de copa america}, {//data de argentino b}]
}

Esta nueva estructura definitivamente nos permite pensar el código async como sync, simplemente utilizando funciones de tipo async.

Tengo que esperar hasta 2050?

No es necesario esperar a que los navegadores adopten esta feature para poder usarla hoy. De hecho si entran ahora al Panel de Control de Mango, van a ejecutarse requests utilizando esta técnica.

Gracias a transpilers como Babel pueden usar async/await en navegadores como IE8. En el caso de Babel, es necesario habilitar la transformación es7.asyncFunctions.

Otro caso de uso

Un caso de uso muy interesante es el uso de funciones async para el acceso a bases de datos en node.js.

El siguiente es un ejemplo, pidiendo prestadas las arrow functions de ES6 y utilizando express y el ORM Sequelize (que hace uso de promesas):

// antes
app.get('/partidos', (req, res) => {
  Partido.findAll({})
  .then(function(partidos){
    res.json(partidos);
  });
});

// usando async functions
app.get('/partidos', async (req, res) => res.json(await Partido.findAll({}) ));

Podemos concluir que esta nueva construcción nos permite creer que estamos ejecutando código de modo síncrono, pero como toda magia es solo una ilusión.

Cualquier duda/comentario o experiencias que quieran compartir, me pueden contactar vía Twitter a @impronunciable.

Los comentarios están cerrados para este artículo.