Funções assíncronas: como tornar as promessas amigáveis

As funções assíncronas permitem escrever código baseado em promessa como se fosse síncrono.

Jake Archibald
Jake Archibald

As funções assíncronas são ativadas por padrão no Chrome, Edge, Firefox e Safari, e são, francamente, maravilhosas. Elas permitem escrever código baseado em promessa como se fosse síncrono, mas sem bloquear a linha de execução principal. Elas tornam o código assíncrono menos "inteligente" e mais legível.

As funções assíncronas funcionam assim:

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

Se você usar a palavra-chave async antes de uma definição de função, poderá usar await dentro da função. Quando você await uma promessa, a função é pausada de uma forma não-bloqueante até que a promessa seja concluída. Se a promessa for cumprida, você vai receber o valor de volta. Se a promessa for rejeitada, o valor rejeitado será descartado.

Suporte ao navegador

Compatibilidade com navegadores

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

Origem

Exemplo: registrar uma busca

Digamos que você queira buscar um URL e registrar a resposta como texto. Confira como fica usando promessas:

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

E aqui está a mesma coisa usando funções assíncronas:

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

É o mesmo número de linhas, mas todas as chamadas de retorno desapareceram. Isso facilita muito a leitura, especialmente para pessoas menos familiarizadas com promessas.

Valores de retorno assíncronos

As funções assíncronas sempre retornam uma promessa, quer você use await ou não. Essa promessa é resolvida com qualquer coisa que a função assíncrona retorne, ou rejeitada com qualquer coisa que a função assíncrona descarte. Assim, com:

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

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

... chamar hello() retorna uma promessa que se cumpre com "world".

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

…chamar foo() retorna uma promessa que rejeita com Error('bar').

Exemplo: streaming de uma resposta

A vantagem das funções assíncronas aumenta em exemplos mais complexos. Digamos que você queira transmitir uma resposta ao registrar as partes e retornar o tamanho final.

Aqui está ela com promessas:

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);
    });
  });
}

Verifique-me, Jake "portador de promessas" Archibald. Vê como estou chamando processResult() dentro de si para configurar um loop assíncrono? Escrever isso me fez sentir muito inteligente. Porém, como com a maioria dos códigos "inteligentes", você tem que analisá-lo por um longo tempo para descobrir o que está fazendo, como uma daquelas imagens de olho-mágico da década de 90.

Vamos tentar novamente com funções assí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 o "inteligente" desapareceu. O loop assíncrono que me fez sentir tão presunçoso foi substituído por um "while-loop" confiável e entediante. Muito melhor. No futuro, você terá iteradores assíncronos, que substituirão o loop while por um loop "for-of", tornando-o ainda mais elegante.

Outra sintaxe de função assíncrona

Já mostramos async function() {}, mas a palavra-chave async pode ser usada com outra sintaxe de função:

Funções de seta

// 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 classe

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(…);

Tenha cuidado! Evite ser sequencial demais

Embora esteja compilando código síncrono, certifique-se de não perder a oportunidade de fazer coisas em paralelo.

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

O item acima leva 1000ms para ser concluído, enquanto:

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!';
}

O item acima leva 500 ms para ser concluído, porque ambas as esperas ocorrem ao mesmo tempo. Vamos conferir um exemplo prático.

Exemplo: como gerar buscas em ordem

Digamos que você queira buscar uma série de URLs e registrá-los o mais rapidamente possível, na ordem correta.

Respira fundo: eis como isso fica com promessas:

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());
}

Sim, isso mesmo, estou usando reduce para ligar de uma sequência de promessas. Eu sou tão inteligente. Mas isso é uma codificação muito inteligente que é melhor evitar.

No entanto, ao converter o item acima para uma função assíncrona, é tentador ser sequencial demais:

Não recomendado: muito sequencial
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
Ficou muito mais elegante, mas minha segunda busca não começa até que a primeira busca tenha sido totalmente lida, e assim por diante. Isso é muito mais lento do que o exemplo de promessas que executa as buscas em paralelo. Felizmente, há um meio termo ideal.
Recomendado: bom e 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);
  }
}
Neste exemplo, os URLs são buscados e lidos em paralelo, mas o bit reduce "inteligente" é substituído por um loop for padrão, chato e legível.

Solução alternativa para suporte do navegador: geradores

Se você tem como alvo navegadores que suportam geradores (que inclui a versão mais recente de todos os principais navegadores ), você pode usar funções assíncronas polyfill.

O Babel vai fazer isso por você. Confira um exemplo usando o Babel REPL

Recomendo a abordagem de transcompilação, porque você pode desativá-la quando os navegadores de destino oferecerem suporte a funções assíncronas, mas se você realmente não quiser usar um transpilador, poderá usar o polyfill do Babel (em inglês) e usá-lo por conta própria. Em vez de:

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

…você incluiria o polyfill e escreveria:

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

Você precisa transmitir um gerador (function*) para createAsyncFunction e usar yield em vez de await. Exceto por isso, funciona da mesma forma.

Solução alternativa: regenerador

Se seu objetivo são navegadores mais antigos, o Babel também pode transcompilar geradores, o que permite usar funções assíncronas até mesmo no IE8. Para fazer isso, você precisa da pré-configuração es2017 do Babel e da pré-configuração es2015.

A saída não é tão bonita, então cuidado com o excesso de código.

Sincronize tudo!

Uma vez que funções assíncronas funcionam em todos os navegadores, use-as em todas as funções que retornam promessas. Elas não apenas tornam seu código mais organizado, mas também garante que a função sempre retorne uma promessa.

Fiquei muito animado com as funções assíncronas em 2014, e é ótimo vê-las chegando, de verdade, nos navegadores. Legal!