Artigo original: JavaScript — from callbacks to async/await
O JavaScript é síncrono. Isso quer dizer que ele executará seu bloco de código na ordem após o hoisting (texto em inglês). Antes de o código ser executado, as declarações de var
e de function
passam pelo processo de "hoisted" (algo como içamento, em português) para o topo do código.
Aqui temos um exemplo de código síncrono:
console.log('1')
console.log('2')
console.log('3')
O código, com segurança, registrará "1 2 3".
Solicitações assíncronas aguardarão o final de uma contagem de tempo ou a resposta de uma solicitação enquanto o resto do código continua a ser executado. Então, quando a hora certa chegar, uma função de callback colocará em ação essas solicitações assíncronas.
Este é um exemplo de código assíncrono:
console.log('1')
setTimeout(function depoisDeDoisSegundos() {
console.log('2')
}, 2000)
console.log('3')
O que aparecerá no console, de fato, será "1 3 2", pois o "2" está em um setTimeout
, que será executado apenas, neste exemplo, após dois segundos. Sua aplicação não fica parada aguardando que os dois segundos passem. Em vez disso, ela segue executando o resto do código e, quando o "timeout" chega ao final, ela retorna à função depoisDeDoisSegundos.
Você pode estar se perguntando "Qual é a utilidade disso?" ou "Como eu faço o meu código assíncrono se tornar síncrono?". Espero poder mostrar para você essas respostas.
"O problema"
Digamos que nosso objetivo é procurar por um usuário do GitHub e obter todos os repositórios daquele usuário. O problema é que não sabemos o nome exato do usuário. Então, temos uma lista com todos os usuários de nomes semelhantes e seus respectivos repositórios.
Não precisa ser nada muito estiloso. Algo assim já serve:

Nestes exemplos, o código de solicitação usará o XHR (XMLHttpRequest). Você pode substituí-lo pelo $.ajax
do jQuery ou a abordagem nativa mais recente, chamada fetch
. Ambas darão a você uma abordagem com promises de saída.
Haverá pequenas alterações, dependendo da sua abordagem, mas, para começar, temos:
// o argumento do url pode ser algo assim: 'https://api.github.com/users/daspinola/repos'
function request(url) {
const xhr = new XMLHttpRequest();
xhr.timeout = 2000;
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// O código que vai aqui é para quando uma resposta do servidor foi um sucesso
} else {
// O código que vai aqui é para quando uma resposta do servidor falhou
}
}
}
xhr.ontimeout = function () {
// Bem, levou um tempo para colocar aqui o código para tratar disso
}
xhr.open('get', url, true)
xhr.send();
}
Lembre-se de que, nesses exemplos, a parte importante não é a forma do resultado final. Em vez disso, seu objetivo deve ser entender as diferenças entre as abordagens e como tirar proveito delas para o seu desenvolvimento.
Callback
É possível salvar uma referência de uma função em uma variável usando o JavaScript. Então, você pode usá-la como argumento de outra função para executá-la mais tarde. É isso que chamamos de "callback".
Um exemplo seria:
// Execute a função "fazIsso" com outra função como parâmetro (nesse caso, "eAgoraIsso". fazIsso executará sempre o código que tem. Quando acabar, eAgoraIsso será executada também.
fazIsso(eAgoraIsso)
// Dentro de "fazIsso" há uma referência a "callback", que é apenas uma variável que terá a referência dessa função.
function eAgoraIsso() {
console.log('e agora isso')
}
// Você pode chamar a variável como você quiser. "callback" é uma abordagem comum
function fazIsso(callback) {
console.log('isso primeiro')
// O '()' ocorre para dizer ao seu código para executar a função de referência. Caso contrário, ela apenas registrará a referência em log
callback()
}
Usar callback
para resolver nosso problema nos permite fazer algo como isso para a função request
que definimos anteriormente:
function request(url, callback) {
const xhr = new XMLHttpRequest();
xhr.timeout = 2000;
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
callback(null, xhr.response)
} else {
callback(xhr.status, null)
}
}
}
xhr.ontimeout = function () {
console.log('Timeout')
}
xhr.open('get', url, true)
xhr.send();
}
Nossa função de solicitação (em inglês, request) agora aceitará uma callback
. Assim, quando uma request
for realizada, a callback será chamada em caso de erro e em caso de sucesso.
const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
request(userGet, function handleUsersList(error, users) {
if (error) throw error
const list = JSON.parse(users).items
list.forEach(function(user) {
request(user.repos_url, function handleReposList(err, repos) {
if (err) throw err
// Trate da lista de repositórios aqui
})
})
})
Vamos analisar o código:
- Fazemos uma solicitação para obter os repositórios do usuário
- Após a solicitação ser concluída, usamos a callback
handleUsersList
- Se não houver erro, fazemos o parsing de nossa resposta do servidor em um objeto usando
JSON.parse
- Em seguida, iteramos nossa lista de usuário, pois é possível que haja mais de um. Para cada usuário, solicitamos sua lista de repositórios. Usaremos o url retornado por usuário em nossa primeira resposta. Chamamos
repos_url
como o url para nossas próximas solicitações ou o que vier da primeira resposta - Quando a solicitação concluir a callback, a chamaremos. Isso tratará do erro ou da resposta com a lista de repositórios daquele usuário.
Observação: enviar o erro primeiro como parâmetro é uma prática comum, especialmente ao usar o Node.js.
Uma abordagem mais "completa" e legível seria fazer um tratamento do erro. Manteríamos a callback separada da execução da solicitação.
Um exemplo seria algo assim:
try {
request(userGet, handleUsersList)
} catch (e) {
console.error('Festa de solicitações! ', e)
}
function handleUsersList(error, users) {
if (error) throw error
const list = JSON.parse(users).items
list.forEach(function(user) {
request(user.repos_url, handleReposList)
})
}
function handleReposList(err, repos) {
if (err) throw err
// Tratar da lista de repositórios aqui
console.log('Meus poucos repositórios', repos)
}
Isso acaba gerando problemas como o 'racing' e questões de tratamento de erros. 'Racing' é o que acontece quando você não controla qual usuário você obterá primeiro. Estamos solicitando as informações para todos eles, no caso de haver mais de um. Não estamos levando a ordem em consideração. Por exemplo, o usuário 10 pode vir primeiro e o usuário 2 no final. Veremos uma solução possível posteriormente neste artigo.
O problema principal com as callbacks é o fato de a manutenção e a legibilidade poderem se tornar complicadas. De fato, elas já são um pouco e o código não faz quase nada. Isso é conhecido como callback hell (ou, em português, o 'inferno das callbacks'), que pode ser evitado com nossa próxima abordagem.

Promises
As promises podem tornar seu código mais legível. Um desenvolvedor novo pode vir na base do código e ver uma ordem clara de execução em seu código.
Para criar uma promise, você pode usar:
const myPromise = new Promise(function(resolve, reject) {
// código aqui
if (codigoBom) {
resolve('tudo bem')
} else {
reject('erro')
}
})
myPromise
.then(function quandoOk(response) {
console.log(response)
return response
})
.catch(function comProblemas(err) {
console.error(err)
})
Vamos analisar passo a passo:
- Uma promise é iniciada com uma
function
que tem as instruçõesresolve
ereject
- Torne seu código assíncrono dentro da função
Promise
. Retorneresolve
quando tudo der certo. Do contrário, retornereject
- Quando
resolve
é encontrado, o método.then
será executado para aquelaPromise
. Quando umreject
for encontrado,.catch
é acionado
Coisas para levar em consideração:
resolve
ereject
aceitam apenas um parâmetroresolve('oba', 'funciona')
enviará apenas 'oba' à função de callback.then
- Se você encadear diversos
.then
, adicione umreturn
se quiser que o valor de.then
seguinte não sejaundefined
- Quando um
reject
é pego com o.catch
se você tiver um.then
encadeado a ele, ele ainda executará aquele.then
. Você pode ver esse.then
como um "sempre executável" e pode conferir um exemplo neste comentário - Com vários
.then
encadeados, se um erro acontece no primeiro, ele saltará os.then
subsequentes até encontrar um.catch
- Uma promise tem três estados:
pending: quando aguarda que umresolve
ou umreject
aconteçam
resolved: quando é resolvida
rejected: quando é rejeitada - Quando a promise estiver no estado
resolved
ourejected
, ela não poderá mais ser alterada
Observação: você pode criar promises sem a função no momento das declarações. A maneira como estou mostrando é somente uma maneira comum de fazê-lo.
"Quanta teoria… já me deixou confuso", você pode até dizer.
Vamos usar nosso exemplo de solicitação com uma promise para tentar esclarecer as coisas:
function request(url) {
return new Promise(function (resolve, reject) {
const xhr = new XMLHttpRequest();
xhr.timeout = 2000;
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(xhr.response)
} else {
reject(xhr.status)
}
}
}
xhr.ontimeout = function () {
reject('timeout')
}
xhr.open('get', url, true)
xhr.send();
})
}
Neste cenário, quando você executa uma request
, ela retornará algo assim:

const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
const myPromise = request(userGet)
console.log('estará pending ao aparecer no console', myPromise)
myPromise
.then(function handleUsersList(users) {
console.log('quando um resolve é encontrado, vem parar aqui com a response, neste caso, os usuários ', users)
const list = JSON.parse(users).items
return Promise.all(list.map(function(user) {
return request(user.repos_url)
}))
})
.then(function handleReposList(repos) {
console.log('Todos os repositórios de usuários em um array', repos)
})
.catch(function handleErrors(error) {
console.log('quando um reject é encontrado, vem parar aqui, ignorando a instrução then ', error)
})
É assim que resolvemos o 'racing' e alguns dos problemas de tratamento de erros. O código ainda é um pouco confuso, mas é uma forma de mostrar para você que essa abordagem ainda pode criar problemas de legibilidade.
Um conserto rápido seria separar as callbacks assim:
const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
const userRequest = request(userGet)
// Uma simples leitura em voz alta dessa parte dá uma boa ideia do que faz o código
userRequest
.then(handleUsersList)
.then(repoRequest)
.then(handleReposList)
.catch(handleErrors)
// callback que trata da lista de usuários
function handleUsersList(users) {
return JSON.parse(users).items
}
// callback que faz a solicitação dos repositórios
function repoRequest(users) {
return Promise.all(users.map(function(user) {
return request(user.repos_url)
}))
}
// callback que trata da lista de repositórios
function handleReposList(repos) {
console.log('All users repos in an array', repos)
}
// callback que trata dos erros
function handleErrors(error) {
console.error('Something went wrong ', error)
}
Só de olhar para o que userRequest
está aguardando na ordem com os .then
, você pode entender o que esperamos desse bloco de código. Tudo é mais ou menos separado por responsabilidade.
Esta é apenas uma amostra do que são as promises. Para entender melhor como elas funcionam, recomendo e muito a leitura deste artigo.
Generators
Outra abordagem é o uso de generators. Isso é um pouco mais avançado, então fique à vontade de pular para o próximo tópico.
Um dos usos dos generators é permitir que você tenha código assíncrono que se parece com código síncrono.
Eles são representados por um *
na função e têm essa aparência:
function* foo() {
yield 1
const args = yield 2
console.log(args)
}
var fooIterator = foo()
console.log(fooIterator.next().value) // mostrará 1 no console
console.log(fooIterator.next().value) // mostrará 2 no console
fooIterator.next('aParam') // registrará o console.log dentro do generator 'aParam'
Em vez de retornar com um return
, os generators têm uma instrução yield
. Ela interromperá a execução da função até que um .next
seja feito para aquela iteração da função. É semelhante à promise com .then
, pois somente executa quando é retornado um resolved.
Nossa função de solicitação (request) tem essa aparência:
function request(url) {
return function(callback) {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
callback(null, xhr.response)
} else {
callback(xhr.status, null)
}
}
}
xhr.ontimeout = function () {
console.log('timeout')
}
xhr.open('get', url, true)
xhr.send()
}
}
Queremos o url
como um argumento. Porém, em vez de executar a solicitação diretamente, nós a queremos apenas quando tivermos uma callback para tratar da resposta.
Nosso generator
terá essa aparência:
function* list() {
const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
const users = yield request(userGet)
yield
for (let i = 0; i<=users.length; i++) {
yield request(users[i].repos_url)
}
}
Ele será responsável por:
- Aguardar até que a primeira
request
seja preparada - Retornar uma referência da
function
esperando umacallback
para a primeirarequest
. Nossa função derequest
aceita umurl
e retorna umafunction
que espera umacallback
- Esperar que os
users
sejam enviados no próximo.next
- Iterar pelos
users
- Aguardar pelo
.next
para cada um dosusers
- Retornar suas respectivas funções de callback
Assim, uma execução teria essa aparência:
try {
const iterator = list()
iterator.next().value(function handleUsersList(err, users) {
if (err) throw err
const list = JSON.parse(users).items
// envia a lista dos usuários para iterator
iterator.next(list)
list.forEach(function(user) {
iterator.next().value(function userRepos(error, repos) {
if (error) throw repos
// Trata de cada repositório de usuário individual aqui
console.log(user, JSON.parse(repos))
})
})
})
} catch (e) {
console.error(e)
}
Podemos separar as funções de callback como fizemos anteriormente. Você já deve ter entendido a ideia agora. Uma lição para se relembrar é que podemos tratar de cada lista de repositórios de cada usuário individualmente.
Não sei bem como eu me sinto quanto aos generators. Por um lado, eu consigo entender o que é esperado do código simplesmente olhado para o generator.
Sua execução, no entanto, acaba tendo problemas semelhantes aos do callback hell.
Como ocorre com async/await, é recomendado usar um compilador. Isso ocorre porque ele não tem o suporte de versões mais antigas de navegadores.
Além disso, na minha experiência, ele não é muito comum. Ele, portanto, pode gerar confusão em bases de código mantidas por diversos desenvolvedores.
Uma ideia ótima sobre o funcionamento dos generators pode ser encontrada neste artigo (em inglês). Aqui, você vê um outro recurso (também em inglês).
Async/Await
Este método parece uma mistura de generators e promises. Você precisa apenas informar ao seu código quais funções serão async
e que parte do código precisará await
(aguardar, em português) até que a promise
seja concluída.
somaVinteDepoisDeDoisSegundos(10)
.then(result => console.log('after 2 seconds', result))
async function somaVinteDepoisDeDoisSegundos(valor) {
const restante = depoisDeDoisSegundos(20)
return valor + await restante
}
function depoisDeDoisSegundos(valor) {
return new Promise(resolve => {
setTimeout(() => { resolve(valor) }, 2000);
});
}
Neste cenário:
- Temos
somaVinteDepoisDeDoisSegundos
como a função assíncrona - Dizemos ao código para aguardar pelo
resolve
oureject
de nossas funções de promisedepoisDeDoisSegundos
- Somente chegaremos ao
.then
quando as operaçõesawait
encerrarem. Nesse caso, temos apenas uma.
Ao aplicar isso à nossa request
, a deixamos como uma promise
, como vimos antes:
function request(url) {
return new Promise(function(resolve, reject) {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(xhr.response)
} else {
reject(xhr.status)
}
}
}
xhr.ontimeout = function () {
reject('timeout')
}
xhr.open('get', url, true)
xhr.send()
})
}
Criamos nossa função async
com as await
necessárias, assim:
async function list() {
const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
const users = await request(userGet)
const usersList = JSON.parse(users).items
usersList.forEach(async function (user) {
const repos = await request(user.repos_url)
handleRepoList(user, repos)
})
}
function handleRepoList(user, repos) {
const userRepos = JSON.parse(repos)
// Trata de cada repositório de usuário individual aqui
console.log(user, userRepos)
}
Até o momento, temos uma função list
assíncrona que tratará das solicitações. Outra async é necessária no forEach
para que tenhamos a lista de repos
de cada usuário para serem manipuladas.
Chamamos isso assim:
list()
.catch(e => console.error(e))
Esta e a abordagem das promises são as minhas favoritas, já que o código é de fácil leitura e alteração. Você pode ler sobre async/await com mais profundidade aqui (texto em inglês).
Um lado negativo de usar async/await é o fato de não ter o suporte no front-end em navegadores antigos nem no back-end. Seria preciso usar, no mínimo, a versão 8 do Node.
Você pode usar um compilador como o babel para ajudar a resolver isso.
"Solução"
Você pode ver o código final, onde conseguimos atender à nossa meta inicial usando async/await neste trecho.
Algo que você pode fazer é experimentar com as diversas formas mencionadas neste artigo.
Conclusão
Dependendo do cenário, você poderá usar:
- async/await
- callbacks
- uma mistura delas
É com você achar o que melhor atende aos seus propósitos e aquilo que ajuda você a manter seu código, de maneira que seja compreensível para os outros e para seu futuro eu.
Observação: qualquer uma das abordagens se torna levemente menos extensa ao usar as alternativas para solicitações, como $.ajax
e fetch
.
Conte-me o que você faria de diferente e as diversas maneiras que você encontrou para tornar cada abordagem mais legível.
Este é o artigo 11 de uma série de 30. Ele é parte de um projeto de publicação de pelo menos um artigo por semana, de ideias a tutoriais. Deixe seu comentário e siga-me no Twitter (Diogo Spínola) e continue com seus projetos brilhantes!