JavaScript async / await: a parte boa, armadilhas e como usar

O async/await 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 artigo, exploraremos 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 async/await trazido para nós é o estilo de programação síncrona. Vamos ver um exemplo.

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

É óbvio que a versão async/await é muito mais fácil de entender do que a versão prometida. Se você ignorar a palavra-chave await , o código será semelhante a qualquer outra linguagem síncrona, como Python.

E o ponto ideal não é apenas a legibilidade. async/await tem suporte nativo ao navegador. A partir de hoje, todos os navegadores tradicionais têm suporte total para funções assíncronas.

Todos os navegadores tradicionais suportam funções Async. (Fonte: https://caniuse.com/ )

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 de await , verá o depurador parar por um breve período enquanto bookModel.fetchAll() executa seu trabalho e, em seguida, passa para a próxima linha .filter ! Isso é muito mais fácil do que o caso da promessa, no qual você precisa configurar outro ponto de interrupção na linha .filter .

Depurando a função assíncrona. O depurador aguardará na fila de espera e passará para a próxima resolvida.

Outro benefício menos óbvio é a palavra async chave async . Ele declara que o valor de retorno da função getBooksByAuthorWithAwait() é garantido como promessa, de modo que os chamadores possam chamar getBooksByAuthorWithAwait().then(...) ou await getBooksByAuthorWithAwait() com segurança. Pense sobre este caso (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 promessa (caso normal) ou um valor null (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 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, que eu respeitosamente discordo. Async / await é uma melhoria, mas não é mais do que um açúcar sintático, o que não vai mudar completamente o nosso estilo de programação.

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

Considere as 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 promessa se você a chamar diretamente.

Bem, isso não é necessariamente uma coisa ruim. Apenas o nome que await 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.

Muito sequencial

Embora await possa fazer com que seu código pareça síncrono, lembre-se de que eles ainda são assíncronos e devem ser tomados cuidados para evitar que sejam muito sequenciais.

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

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

  1. await bookModel.fetchAll() irá esperar até que fetchAll() retorne.
  2. await authorModel.fetch(authorId) 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, estas duas chamadas tornam-se sequenciais 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 = aguardar bookPromise;
const author = await authorPromise;
Retorna {
autor,
books: books.filter (book => book.authorId === authorId),
};
}

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

 getAuthors assíncrono (authorIds) { 
// ERRADO, isso causará chamadas sequenciais
// const autores = _.map (
// authorIds,
// id => aguardar authorModel.fetch (id));
 // CORRETO 
prom promises = _.map (authorIds, id => authorModel.fetch (id));
autores const = esperam Promise.all (promessas);
}

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 promessas diretamente.

Tratamento de erros

Com promessas, 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.

tente … pegar

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

 class BookModel { 
fetchAll () {
return new Promise ((resolver, rejeitar) => {
window.setTimeout (() => {reject ({'error': 400})}, 1000);
});
}
}
 // async / await 
async getBooksByAuthorWithAwait (authorId) {
experimentar {
const books = aguardar bookModel.fetchAll ();
} pegar (erro) {
console.log (erro); // {"error": 400}
}

O erro catch é 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 return no bloco catch é equivalente a usar return undefined; e também é um valor normal.)
  • Jogue, se você quiser que o chamador lide com isso. Você pode jogar o objeto de erro simples diretamente como throw error; , que permite que você use essa função async getBooksByAuthorWithAwait() em uma cadeia de promessas (ou seja, você ainda pode chamá-la como getBooksByAuthorWithAwait().then(...).catch(error => ...) ); Ou você pode envolver o erro com o objeto Error , como throw new Error(error) , que dará o rastreamento completo da pilha quando esse erro for exibido no console.
  • Rejeitar, como return Promise.reject(error) . 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 outras linguagens como Java ou C ++, você não terá dificuldade em entender isso.
  • Você ainda pode agrupar várias chamadas de await em uma única try...catch block para manipular erros em um local, se a manipulação de erros por etapa não for necessária.

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

 class BookModel { 
fetchAll () {
cb (); // note que `cb` é indefinido e resultará em uma exceção
return fetch ('/ books');
}
}
 experimentar { 
bookModel.fetchAll ();
} pegar (erro) {
console.log (erro); // Isto irá imprimir "cb não está definido"
}

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.

Fazendo funções retornam o valor

Outra maneira de lidar com erros é inspirada na linguagem Go. Permite que a função assíncrona retorne o erro e o resultado. Veja este post para os detalhes:

Como escrever async aguardar sem blocos try-catch em JavaScript
ES7 Async / await nos permite, como desenvolvedores, escrever códigos JS assíncronos que parecem síncronos. Na versão atual do JS, nós… blog.grossman.io

Em suma, você pode usar a função assíncrona assim:

 [err, user] = await to(UserModel.findById(1)); 

Pessoalmente, não gosto dessa abordagem, pois ela traz o estilo Go para JavaScript, o que não é natural, mas, em alguns casos, isso pode ser bastante útil.

Usando o .catch

A abordagem final que introduziremos aqui é continuar usando o .catch() .

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

 // books === undefined se o erro acontecer, 
// pois nada retornou na instrução catch
deixe livros = aguardar bookModel.fetchAll ()
.catch ((erro) => {console.log (erro);});

Existem dois problemas menores nessa abordagem:

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

Texto original em inglês.