Funciones asíncronas: hacer promesas amigables

Las funciones asíncronas te permiten escribir código basado en promesas como si fuera síncrono.

Las funciones asíncronas están habilitadas de forma predeterminada en Chrome, Edge, Firefox y Safari, y son realmente maravillosas. Te permiten escribir código basado en promesas como si fuera síncrono, pero sin bloquear el subproceso principal. Hacen que tu código asíncrono sea menos “inteligente” y más legible.

Las funciones asíncronas funcionan de la siguiente manera:

async function myFirstAsyncFunction() {
  try {
    const fulfilledValue = await promise;
  } catch (rejectedValue) {
    // …
  }
}

Si usas la palabra clave async antes de una definición de función, puedes usar await dentro de la función. Cuando await una promesa, la función se pausa de manera no bloqueante hasta que se resuelve la promesa. Si se cumple la promesa, recuperas el valor. Si se rechaza la promesa, se arroja el valor rechazado.

Navegadores compatibles

Browser Support

  • Chrome: 55.
  • Edge: 15.
  • Firefox: 52.
  • Safari: 10.1.

Source

Ejemplo: Cómo registrar una recuperación

Supongamos que quieres recuperar una URL y registrar la respuesta como texto. Así se ve con promesas:

function logFetch(url) {
  return fetch(url)
    .then((response) => response.text())
    .then((text) => {
      console.log(text);
    })
    .catch((err) => {
      console.error('fetch failed', err);
    });
}

Y esto es lo mismo con funciones asíncronas:

async function logFetch(url) {
  try {
    const response = await fetch(url);
    console.log(await response.text());
  } catch (err) {
    console.log('fetch failed', err);
  }
}

Es la misma cantidad de líneas, pero no hay devoluciones de llamada. Esto facilita mucho la lectura, especialmente para quienes no están tan familiarizados con las promesas.

Valores de retorno asíncronos

Las funciones asíncronas siempre muestran una promesa, ya sea que uses await o no. Esa promesa se resuelve con lo que devuelve la función asíncrona o se rechaza con lo que arroja. Por lo tanto, con lo siguiente:

// wait ms milliseconds
function wait(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

async function hello() {
  await wait(500);
  return 'world';
}

… llamar a hello() muestra una promesa que cumple con "world".

async function foo() {
  await wait(500);
  throw Error('bar');
}

… llamar a foo() muestra una promesa que rejects con Error('bar').

Ejemplo: Cómo transmitir una respuesta

El beneficio de las funciones asíncronas aumenta en ejemplos más complejos. Supongamos que quieres transmitir una respuesta mientras registras los fragmentos y mostrar el tamaño final.

Aquí está con promesas:

function getResponseSize(url) {
  return fetch(url).then((response) => {
    const reader = response.body.getReader();
    let total = 0;

    return reader.read().then(function processResult(result) {
      if (result.done) return total;

      const value = result.value;
      total += value.length;
      console.log('Received chunk', value);

      return reader.read().then(processResult);
    });
  });
}

Soy Jake “el que cumple promesas” Archibald. ¿Ves cómo llamo a processResult() dentro de sí mismo para configurar un bucle asíncrono? Escribir eso me hizo sentir muy inteligente. Sin embargo, como la mayoría de los códigos “inteligentes”, debes mirarlo durante mucho tiempo para descubrir qué hace, como una de esas imágenes de ojos mágicos de los años 90.

Volvamos a intentarlo con funciones asíncronas:

async function getResponseSize(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  let result = await reader.read();
  let total = 0;

  while (!result.done) {
    const value = result.value;
    total += value.length;
    console.log('Received chunk', value);
    // get the next result
    result = await reader.read();
  }

  return total;
}

Se pierde toda la “inteligencia”. El bucle asíncrono que me hizo sentir tan orgulloso se reemplaza por un bucle while confiable y aburrido. Mucho mejor. En el futuro, obtendrás iteradores asíncronos, que reemplazarán el bucle while por un bucle for-of, lo que lo hará aún más ordenado.

Otra sintaxis de funciones asíncronas

Ya te mostré async function() {}, pero la palabra clave async se puede usar con otra sintaxis de función:

Funciones de flecha

// map some URLs to json-promises
const jsonPromises = urls.map(async (url) => {
  const response = await fetch(url);
  return response.json();
});

Métodos de objetos

const storage = {
  async getAvatar(name) {
    const cache = await caches.open('avatars');
    return cache.match(`/avatars/${name}.jpg`);
  }
};

storage.getAvatar('jaffathecake').then();

Métodos de clase

class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }

  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
  }
}

const storage = new Storage();
storage.getAvatar('jaffathecake').then();

Debe tener cuidado. Evita ser demasiado secuencial

Aunque escribas código que parezca síncrono, asegúrate de no perder la oportunidad de hacer cosas en paralelo.

async function series() {
  await wait(500); // Wait 500ms…
  await wait(500); // …then wait another 500ms.
  return 'done!';
}

Lo anterior tarda 1,000 ms en completarse, mientras que:

async function parallel() {
  const wait1 = wait(500); // Start a 500ms timer asynchronously…
  const wait2 = wait(500); // …meaning this timer happens in parallel.
  await Promise.all([wait1, wait2]); // Wait for both timers in parallel.
  return 'done!';
}

Lo anterior tarda 500 ms en completarse, ya que ambas esperas se producen al mismo tiempo. Veamos un ejemplo práctico.

Ejemplo: Cómo generar recuperaciones en orden

Supongamos que quieres recuperar una serie de URLs y registrarlas lo antes posible, en el orden correcto.

Respira profundo: Así se ve con promesas:

function markHandled(promise) {
  promise.catch(() => {});
  return promise;
}

function logInOrder(urls) {
  // fetch all the URLs
  const textPromises = urls.map((url) => {
    return markHandled(fetch(url).then((response) => response.text()));
  });

  // log them in order
  return textPromises.reduce((chain, textPromise) => {
    return chain.then(() => textPromise).then((text) => console.log(text));
  }, Promise.resolve());
}

Sí, es correcto, estoy usando reduce para encadenar una secuencia de promesas. Soy muy inteligente. Sin embargo, esta es una codificación muy inteligente que es mejor no usar.

Sin embargo, cuando conviertes lo anterior en una función asíncrona, es tentador ser demasiado secuencial:

No se recomienda, ya que es demasiado secuencial.
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
Se ve mucho más ordenado, pero mi segunda recuperación no comienza hasta que se lee por completo la primera, y así sucesivamente. Esto es mucho más lento que el ejemplo de promesas que realiza las recuperaciones en paralelo. Por suerte, hay un punto medio ideal.
Recomendado: Es agradable y paralelo.
function markHandled(...promises) {
  Promise.allSettled(promises);
}

async function logInOrder(urls) {
  // fetch all the URLs in parallel
  const textPromises = urls.map(async (url) => {
    const response = await fetch(url);
    return response.text();
  });

  markHandled(...textPromises);

  // log them in sequence
  for (const textPromise of textPromises) {
    console.log(await textPromise);
  }
}
En este ejemplo, las URLs se recuperan y se leen en paralelo, pero el bit “inteligente” reduce se reemplaza por un bucle for estándar, aburrido y legible.

Solución alternativa de compatibilidad con navegadores: generadores

Si te orientas a navegadores que admiten generadores (lo que incluye la versión más reciente de todos los navegadores principales), puedes usar un tipo de polyfill para las funciones asíncronas.

Babel lo hará por ti. Este es un ejemplo a través de la REPL de Babel.

Recomiendo el enfoque de transpilación, ya que puedes desactivarlo una vez que los navegadores de destino admitan funciones asíncronas. Sin embargo, si realmente no quieres usar un transpilador, puedes tomar el polyfill de Babel y usarlo por tu cuenta. En lugar de esta sintaxis:

async function slowEcho(val) {
  await wait(1000);
  return val;
}

…incluirías el polyfill y escribirías lo siguiente:

const slowEcho = createAsyncFunction(function* (val) {
  yield wait(1000);
  return val;
});

Ten en cuenta que debes pasar un generador (function*) a createAsyncFunction y usar yield en lugar de await. Aparte de eso, funciona igual.

Solución alternativa: regenerator

Si te orientas a navegadores más antiguos, Babel también puede transpilar generadores, lo que te permite usar funciones asíncronas hasta IE8. Para ello, necesitas el parámetro de configuración es2017 de Babel y el parámetro de configuración es2015.

El resultado no es tan atractivo, así que ten cuidado con el aumento de código.

Haz todo de forma asíncrona.

Una vez que las funciones asíncronas lleguen a todos los navegadores, úsalas en todas las funciones que devuelvan promesas. No solo hacen que tu código sea más ordenado, sino que se asegura de que esa función siempre devuelva una promesa.

Me entusiasmé mucho con las funciones asíncronas en 2014 y es genial verlas en los navegadores. ¡Genial!