JavaScript async/await: Saiba como usar e não cair em armadilhas #iniciante

Tempo de leitura: 8 minutos

O async/await introduzido pelo ES7 é uma melhoria fantástica na programação assíncrona com JavaScript. Ele forneceu uma opção de usar o código de estilo síncrono para acessar resoruces de forma assíncrona, sem bloquear o thread principal. No entanto, é um pouco complicado usá-lo bem. Neste post, exploraremos o async/await de diferentes perspectivas e mostraremos como usá-las de maneira correta e eficaz.

A parte boa em async/await

O benefício mais importante do async/await trazido para nós é o estilo de programação síncrona. Vamos ver um exemplo.

// async/await
async getBooksByAuthorWithAwait(authorId) {
  const books = await bookModel.fetchAll();
  return books.filter(b => b.authorId === authorId);
}
// promise
getBooksByAuthorWithPromise(authorId) {
  return bookModel.fetchAll()
    .then(books => books.filter(b => b.authorId === authorId));
}

É óbvio que o async/await é uma versão é muito mais fácil de entender do que a versão de promise que é um conceito bastante difuncido no mundo JavaScript. Se você ignorar a palavra-chave await, o código será semelhante a qualquer outro idioma síncrono, como o de Python. E o ponto ideal não é apenas legibilidade o async/await tem suporte nativo ao navegador. Praticamente hoje todos os navegadores tradicionais têm suporte total para funções assíncronas.

Suporte nativo significa que você não precisa transpilar o código. Mais importante, facilita a depuração. Quando você definir um ponto de interrupção no ponto de entrada da função e passar pela linha await, verá o depurador parar por algum tempo enquanto o bookModel.fetchAll() executa seu trabalho e, em seguida, passa para a próxima linha .filter. Isso é muito mais fácil do que o caso de promise, no qual você precisa configurar outro ponto de interrupção na linha .filter.

Outro benefício menos óbvio é a palavra-chave async. Ela declara que o valor de retorno da função getBooksByAuthorWithAwait() é garantido como uma promise, de modo que os chamadores possam chamar getBooksByAuthorWithAwait(), e (…) ou aguardar getBooksByAuthorWithAwait() com segurança. Pense sobre este caso abaixo (má prática!):

getBooksByAuthorWithPromise(authorId) {
  if (!authorId) {
    return null;
  }
  return bookModel.fetchAll()
    .then(books => books.filter(b => b.authorId === authorId));
}

No código acima, getBooksByAuthorWithPromise pode retornar uma promise (caso normal) ou um valor nulo (caso excepcional), caso em que o chamador não pode chamar .then () com segurança. Com a declaração async, torna-se impossível para este tipo de código.

async/await pode ser enganador

Alguns artigos e post na internet comparam o async/await com o promise e afirmam que é a próxima geração na evolução da programação assíncrona do JavaScript, eu particulamente discordo respeitosamente e pra mim async/await é uma melhoria, não passando mais do que um açúcar sintético e que não vai mudar completamente o nosso estilo de programação.

Essencialmente, as funções async ainda são promise. Você tem que entender as promises antes de poder usar as funções async corretamente, e pior ainda, na maioria das vezes você precisa usar promise junto com funções async.

Considere as funções getBooksByAuthorWithAwait() e getBooksByAuthorWithPromises() no exemplo acima. Note que eles não são apenas idênticos funcionalmente, eles também têm exatamente a mesma interface!

Isso significa que getBooksByAuthorWithAwait() retornará uma promise se você a chamar diretamente. Bem, isso não é necessariamente uma coisa ruim. Apenas o nome que aguardava dá às pessoas a sensação de que “Oh, isso pode converter funções assíncronas em funções síncronas”, o que é realmente errado.

async/await pitfalls

Então, quais erros podem ser cometidos ao usar async/await ? Aqui estão alguns comuns.

Too Sequential

Embora await possa fazer com que seu código pareça síncrono, lembre-se de que eles ainda são assíncronos e deve-se tomar cuidado para evitar ser too sequential.

async getBooksAndAuthor(authorId) {
  const books = await bookModel.fetchAll();
  const author = await authorModel.fetch(authorId);
  return {
    author,
    books: books.filter(book => book.authorId === authorId),
  };
}

Este código parece logicamente correto. No entanto isso está errado.

  1. await bookModel.fetchAll() vai esperar até fetchAll() retornar.
  2. Então await authorModel.fetch(authorId) será chamado.

Observe que authorModel.fetch(authorId) não depende do resultado de bookModel.fetchAll() e, de fato, eles podem ser chamados em paralelo! No entanto, usando await aqui, essas duas chamadas se tornam seqüenciais e o tempo total de execução será muito maior do que a versão paralela.

Aqui está o caminho correto:

async getBooksAndAuthor(authorId) {
  const bookPromise = bookModel.fetchAll();
  const authorPromise = authorModel.fetch(authorId);
  const book = await bookPromise;
  const author = await authorPromise;
  return {
    author,
    books: books.filter(book => book.authorId === authorId),
  };
}

Ou, pior ainda, se você quiser buscar uma lista de itens um por um, você precisa confiar em promise:

async getAuthors(authorIds) {
  // ERRADO, isso causará chamadas sequenciais
  // const authors = _.map(
  //   authorIds,
  //   id => await authorModel.fetch(id));
// CORRETO
  const promises = _.map(authorIds, id => authorModel.fetch(id));
  const authors = await Promise.all(promises);
}

Em suma, você ainda precisa pensar sobre os fluxos de trabalho de forma assíncrona e, em seguida, tentar escrever o código de forma síncrona com o await. No fluxo de trabalho complicado, pode ser mais fácil usar promise de maneira diretamente.

error Handling

Com promise, uma função assíncrona tem dois valores de retorno possíveis: valor resolvido e valor rejeitado. E podemos usar .then() para casos normais e .catch() para casos excepcionais. No entanto, com o tratamento de erros async/await pode ser complicado.

try…catch

A maneira mais padrão (e recomendada) é usar o comando try … catch. Quando aguardar uma chamada, qualquer valor rejeitado será lançado como uma exceção. Aqui está um exemplo:

class BookModel {
  fetchAll() {
    return new Promise((resolve, reject) => {
      window.setTimeout(() => { reject({'error': 400}) }, 1000);
    });
  }
}
// async/await
async getBooksByAuthorWithAwait(authorId) {
try {
  const books = await bookModel.fetchAll();
} catch (error) {
  console.log(error);    // { "error": 400 }
}

O erro capturado é exatamente o valor rejeitado. Depois que pegamos a exceção, temos várias maneiras de lidar com isso:

  • Manipule a exceção e retorne um valor normal. (Não usar nenhuma instrução de retorno no bloco catch é equivalente a usar return undefined; e também é um valor normal.)
  • Throw, se você quiser que o chamador lide com isso. Você pode jogar o objeto de erro simples diretamente como throw error ; que permite usar essa função assync getBooksByAuthorWithAwait() em uma cadeia de promisses (ou seja, você ainda pode chamá-la como getBooksByAuthorWithAwait().then (…).catch ( erro => …)); Ou você pode quebrar o erro com o objeto Error, como um throw new Error(error), que dará o rastreamento completo da pilha quando esse erro for exibido no console.
  • Rejeitar, como retornar Promise.reject (erro). Isso é equivalente a throw error, portanto não é recomendado.

Os benefícios de usar o try … catch são:

  • Simples e tradicional. Contanto que você tenha experiência em outros idiomas, como Java ou C ++, você não terá dificuldade em entender isso.
  • Você ainda pode agrupar várias chamadas de espera em uma única try…catch block para manipular erros em um local, se o tratamento de erros por etapa não for necessário.

Há também uma falha nessa abordagem. Já que try … catch irá capturar todas as exceções no bloco, algumas outras exceções que normalmente não são capturadas por promises serão capturadas. Pense neste exemplo:

class BookModel {
  fetchAll() {
    cb();    // note `cb` é indefinido e resultará em uma exceção
    return fetch('/books');
  }
}
try {
  bookModel.fetchAll();
} catch(error) {
  console.log(error);  // Isto irá imprimir "cb is not defined"
}

Execute este código e você receberá um erro ReferenceError: cb is not defined no console, na cor preta. O erro foi gerado pelo console.log (), mas não pelo JavaScript em si. Às vezes, isso pode ser fatal: se o BookModel está profundamente envolvido em uma série de chamadas de função e uma das chamadas engole o erro, então será extremamente difícil encontrar um erro indefinido como este.

usando o .catch

A abordagem final que introduziremos aqui é continuar usando .catch(). Lembre-se da funcionalidade await: ele aguardará uma promise para concluir seu trabalho. Lembre-se também de que o promise.catch() também retornará uma promise! Então, podemos escrever o tratamento de erros assim:

// books === undefined se o erro acontecer,
// desde que nada retornou na instrução catch
let books = await bookModel.fetchAll()
  .catch((error) => { console.log(error); });

Existem dois problemas menores nessa abordagem:

1.É uma mistura de promises e funções async. Você ainda precisa entender como as promessas funcionam para lê-lo.
2.O tratamento de erros vem antes do caminho normal, o que não é intuitivo.

Nota final

As palavras-chave async/await introduzidas pelo ES7 são definitivamente uma melhoria para a programação assíncrona do JavaScript. Pode tornar o código mais fácil de ler e depurar. No entanto, a fim de usá-los corretamente, é preciso entender completamente as promessas, uma vez que elas não são mais que açúcar sintático, e a técnica subjacente ainda é promises. Espero que este post lhe dê algumas idéias sobre async/await, e pode ajudá-lo a evitar alguns erros comuns. Obrigado pela sua leitura!!

Gostou do conteúdo? não deixe de seguir a uebile nas redes sociais, pois toda semana tem post novo aqui no blog com mais dicas para o seu impulso digital.