Funciones asíncronas: hacer promesas amigables

Las funciones asincrónicas 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 un 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 asincrónicas 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 aplicas await a una promesa, se detiene la función de una manera sin bloqueo hasta que se establece la promesa. Si la promesa se cumple, recibes el valor. Si se rechaza la promesa, se arroja el valor rechazado.

Navegadores compatibles

Navegadores compatibles

  • 55
  • 15
  • 52
  • 10.1

Origen

Ejemplo: registra una recuperación

Supongamos que quieres 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 asincrónicas:

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 más fácil de leer, especialmente para quienes no están 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 resuelve con lo que muestre la función asíncrona o 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 cumple con "world".

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

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

Ejemplo: transmitir una respuesta

El beneficio de las funciones asincrónicas aumenta en ejemplos más complejos. Supongamos que deseas transmitir una respuesta mientras sales 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 durante años para descubrir lo que hace, como esas imágenes de ojo mágico de los 90.

Intentémoslo 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 asincrónico que me hizo sentir tan presumido se reemplaza por un confiable y aburrido bucle while. Mucho mejor. En el futuro, tendrás iteradores asíncronos, que reemplazarán el bucle while con un bucle for-of, lo que lo hará aún más prolijo.

Otra sintaxis de función asíncrona

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 estés escribiendo un código que luce síncrono, asegúrate de no perder la oportunidad de realizar 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 resultados de recuperaciones en orden

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

Respira 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. Pero esta es una codificación tan inteligente sin la cual es conveniente que no hagas nada.

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

No se recomienda: hay demasiadas secuencias
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 la segunda recuperación no comienza hasta que se haya leído por completo mi primera recuperación, y así sucesivamente. Esto es mucho más lento que el ejemplo de promesas que realiza las recuperaciones en paralelo. Afortunadamente, hay un punto medio ideal.
Recomendado: 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 reduce “inteligente” se reemplaza por un bucle for estándar, aburrido y legible.

Solución de asistencia para el navegador: Generadores

Si te orientas a navegadores que admiten generadores (que incluye la última versión de todos los navegadores principales), puedes usar polyfill para funciones asíncronas.

Babel lo hará por ti. Aquí tienes un ejemplo mediante el REPL de Babel.

Te recomendamos el enfoque transpilado, 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 de la misma manera.

Solución alternativa: regenerador

Si orientas tu app a navegadores más antiguos, Babel también puede transpilar generadores, lo que te permite usar funciones asíncronas hasta en 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 al sobredimensionamiento de código.

¡Haz todo asincrónico!

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

Me fascinaban las funciones asíncronas en 2014 y es genial poder verlas en los navegadores de verdad. ¡Uy!