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 francamente 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 una manera sin bloqueo hasta que se establezca la promesa. Si la promesa se cumple, recibes el valor de vuelta. Si se rechaza la promesa, se genera el valor rechazado.

Navegadores compatibles

Navegadores compatibles

  • 55
  • 15
  • 52.
  • 10.1

Origen

Ejemplo: registro de una recuperación

Supongamos que deseas recuperar una URL y registrar la respuesta como texto. Así se ve el uso de promesas:

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

Y aquí se ve lo mismo con el uso de 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 desaparecen todas las devoluciones de llamada. Esto hace que sea mucho más fácil de leer, especialmente para quienes están menos familiarizados con las promesas.

Valores que se muestran asíncronos

Las funciones asíncronas siempre muestran una promesa, ya sea que uses await o no. Esa promesa se resuelve con lo que muestre la función asíncrona o se rechaza con lo que sea que arroje la función asíncrona. Entonces:

// 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 se completa con "world".

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

...llamar a foo() muestra una promesa que se rechaza con Error('bar').

Ejemplo: transmisión de una respuesta

El beneficio de las funciones asíncronas aumenta en ejemplos más complejos. Supongamos que deseas transmitir una respuesta mientras cierras el registro de 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);
    });
  });
}

Mira, Jake "poseedor de 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. Pero como con la mayoría de los códigos "inteligentes", tienes que mirarlos fijamente durante años para descubrir qué hace, como una de esas fotos de ojo mágico de los 90.

Probemos de nuevo 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;
}

Todo lo "inteligente" desapareció. El bucle asíncrono que me hizo tan presumido se reemplaza por un confiable y aburrido bucle while. 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 prolijo.

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 objeto

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 generar demasiadas secuencias

Aunque escribas código que luce síncrono, asegúrate de no perder la oportunidad de hacer tareas 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 porque ambas esperas ocurren al mismo tiempo. Veamos un ejemplo práctico.

Ejemplo: muestra recuperaciones en orden

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

Respire profundo: así es como se ve eso 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í, así es, estoy usando reduce para encadenar una secuencia de promesas. Soy tan inteligente. Sin embargo, esta programación es un poco tan inteligente sin la cual es mejor.

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

No se recomienda porque 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 prolijo, pero mi segunda recuperación no comienza hasta que la primera se haya leído por completo, y así sucesivamente. Esto es mucho más lento que el ejemplo de promesas que realiza las recuperaciones en paralelo. Afortunadamente, hay un punto intermedio ideal.
Recomendada: 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 para la compatibilidad con navegadores: generadores

Si orientas tu app a navegadores compatibles con generadores (que incluye la versión más reciente de todos los navegadores principales), puedes usar algunas funciones asíncronas de polyfill.

Babel lo hará por ti. Aquí tienes un ejemplo a través del REPL de Babel.

Recomiendo el enfoque de transpilación, porque puedes desactivarlo una vez que tus navegadores de destino admitan funciones asíncronas, pero 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;
}

deberías incluir el polyfill y escribir 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 de la misma manera.

Solución alternativa: regenerador

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 ajuste predeterminado es2017 de Babel y el ajuste predeterminado es2015.

El resultado no es tan bonito, así que presta atención a los aumentos de código.

Haz todo asincrónico.

Una vez que las funciones asíncronas llegan a todos los navegadores, úsalas en cada función que muestra promesas. No solo hacen que tu código sea más ordenado, sino que se aseguran de que la función siempre muestre una promesa.

Me encantó muchísimo las funciones asíncronas en 2014 y es genial verlas en los navegadores. ¡Uy!