JavaScript assíncrono moderno com Async e Await

Introdução

O JavaScript evoluiu de callbacks para promises (ES2015) em um curto período de tempo, e desde o ES2017, JavaScript assíncrono está ainda mais simples com a sintaxe async/await.

Funções async são uma combinação de promises e generators, e basicamente, elas são uma abstração de alto nível das promises. Permita-me repetir: async/await é feito com promises.

Por que a sintaxe async/await foi criada?

Ela reduz o amontoado de código em torno das prommises e a limitação de "não quebre a corrente" das promises encadeadas.

Quando as Promises foram introduzidas no ES2016, elas foram feitas para resolver um problema com código assíncrono, e elas fizeram isso, mas ao longos dos 2 anos que separaram o ES2015 do ES2017, ficou claro que promises poderiam não ser a solução definitiva.

Promises foram introduzidas para resolver o famoso problema de callback hell, mas elas introduziram complexidade por si só, e complexidade sintática.

Elas foram boas primitivas enquanto uma sintaxe melhor não surgia, então, quando chegou a hora certa, obtivemos as funções async.

Elas fazem o código parecer síncrono, mas por baixo dos panos ele é assíncrono e não bloqueante.

Como isso funciona

Uma função async retorna uma promise, como nesse exemplo:

const doSomethingAsync = () => {
return new Promise(resolve => {
setTimeout(() => resolve('I did something'), 3000)
})
}

Quando você quer chamar essa função você prexifa um await, e o código corrente vai parar até que a promise seja resolvida ou rejeitada. Uma ressalva: a função deve ser definida como async. Segue um exemplo:

const doSomething = async () => {
console.log(await doSomethingAsync())
}

Um exemplo rápido

Esse é um exemplo bem simples de async/await sendo usado para rodar uma função assíncronamente:

"Promissifique" tudo

Prefixar o termo async em qualquer função implica que a função vai retornar uma promise.

Mesmo que isso não seja feito tão explicitamente, internamente fará com que retorne uma promise.

Por isso que esse código é válido:

const aFunction = async () => {
return 'test'
}
aFunction().then(alert) // Isso vai invocar um alert com o texto 'test'

e o mesmo aqui:

const aFunction = async () => {
return Promise.resolve('test')
}
aFunction().then(alert) // Isso vai invocar um alert com o texto 'test'

O código é muito mais simples de ler

Como você pôde ver no exemplo acima, nosso código parece muito simples. Compare-o com os códigos usando promises simples, encadeamento e funções callback.

E esse é um exemplo bem simples, os maiores benefícios vão aparecer quando o código for mais complexo.

Por exemplo, aqui temos um código de como obter um recurso JSON e "parsear", usando promises:

const getFirstUserData = () => {
return fetch('/users.json') // pega a lista de users
.then(response => response.json()) // parse JSON
.then(users => users[0]) // pega o primeiro usuário
.then(user => fetch(`/users/${user.name}`)) // pega os dados desse usuário
.then(userResponse => userResponse.json()) // parse JSON
}
getFirstUserData()

E aqui temos a mesma funcionalidade usando async/await:

const getFirstUserData = async () => {
const response = await fetch('/users.json') // pega a lista de users
const users = await response.json() // parse JSON
const user = users[0] // pega o primeiro usuário
const userResponse = await fetch(`/users/${user.name}`) // pega os dados desse usuário
const userData = await userResponse.json() // parse JSON
return userData
}
getFirstUserData()

Múltiplas funções async em série

Funções async podem ser encadeadas facilmente, e a sintaxe é muito mais legível do que usando promises simples:

Mais fácil de debugar

É difícil debugar promises porque o debugger não passa por cima de código assíncrono.

Já Async/await facilita muito isso porque para o compilador é como se fosse apenas código síncrono.