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

As funções assíncronas permitem escrever código baseado em promessas 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 que você escreva código baseado em promessas como se fosse síncrono, mas sem bloquear a linha de execução principal. Elas tornam seu 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 forma não bloqueante até que a promessa seja resolvida. Se a promessa for cumprida, você vai receber o valor de volta. Se a promessa for rejeitada, o valor rejeitado será gerado.

Suporte ao navegador

Browser Support

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

Source

Exemplo: como 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 todos os callbacks foram removidos. Isso facilita a leitura, especialmente para quem não está familiarizado com promessas.

Valores de retorno assíncronos

As funções assíncronas sempre retornam uma promessa, seja com await ou não. Essa promessa é resolvida com o que a função assíncrona retorna ou rejeita com o que a função assíncrona gera. Então, 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 cumpre com "world".

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

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

Exemplo: transmissão de uma resposta

O benefício das funções assíncronas aumenta em exemplos mais complexos. Digamos que você queira transmitir uma resposta enquanto sai dos blocos e retorna o tamanho final.

Confira com as 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);
    });
  });
}

Sou Jake Archibald, o "executor de promessas". Percebeu como estou chamando processResult() dentro de si mesmo para configurar um loop assíncrono? Escrever isso me fez sentir muito inteligente. Mas, como a maioria dos códigos "inteligentes", você precisa olhar para ele por muito tempo para descobrir o que ele está fazendo, como uma daquelas imagens de olho mágico dos anos 90.

Vamos tentar de novo 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;
}

A "inteligência" desapareceu. O loop assíncrono que me deixava tão satisfeito é substituído por um loop while confiável e chato. Muito melhor. No futuro, você vai ter iteradores assíncronos, que vão substituir o loop while por um loop for-of, deixando tudo ainda mais organizado.

Outra sintaxe de função assíncrona

Já mostramos async function() {}, mas a palavra-chave async pode ser usada com outras 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 muito sequencial

Embora você esteja escrevendo um código que parece síncrono, não perca a oportunidade de fazer coisas em paralelo.

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

A instrução acima leva 1.000 ms para ser concluída, 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 exemplo acima leva 500 ms para ser concluído, porque as duas esperas acontecem 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 fazer o registro deles o mais rápido possível, na ordem correta.

Respire fundo: confira como isso fica com as 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());
}

Isso mesmo, estou usando reduce para encadear uma sequência de promessas. Eu sou tão inteligente. Mas isso é um pouco tão inteligente de programação que você não precisa.

No entanto, ao converter o código acima em uma função assíncrona, é tentador usar muita sequência:

Não recomendado: muito sequencial
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
Parece muito mais organizado, mas a segunda busca não começa até que a primeira seja totalmente lida, e assim por diante. Isso é muito mais lento do que o exemplo de promessas que realiza 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 de suporte a navegadores: geradores

Se você estiver segmentando navegadores que oferecem suporte a geradores (incluindo a versão mais recente de todos os principais navegadores), é possível usar uma espécie de polifill para funções assíncronas.

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

Recomendo a abordagem de transpilação, porque você pode desativá-la quando os navegadores de destino oferecerem suporte a funções assíncronas. No entanto, se você realmente não quiser usar um transpilador, use a polyfill do Babel 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;
});

É necessário transmitir um gerador (function*) para createAsyncFunction e usar yield em vez de await. Fora isso, o funcionamento é o mesmo.

Solução alternativa: regenerador

Se você estiver segmentando navegadores mais antigos, o Babel também poderá transpilinar geradores, permitindo que você use funções assíncronas até o IE8. Para fazer isso, você precisa da predefinição es2017 do Babel e da predefinição es2015.

A saída não é tão bonita, então fique atento ao inchaço do código.

Async tudo!

Quando as funções assíncronas forem lançadas em todos os navegadores, use-as em todas as funções que retornam promessas. Elas não apenas deixam seu código mais organizado, mas também garantem que a função sempre retornará uma promessa.

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