Desde que Promises adentraram na vida dos programadores JavaScript elas facilitaram muitas coisas ao mesmo tempo que também causam diversos resultados inesperados para aqueles que ainda não as dominam com certa maestria.
Um desses casos inesperados é quando você tem que lidar com mais de uma Promise ao mesmo tempo, por exemplo enviar várias requisições de forma sequencial ou enviar elas de forma paralela, e muitos tem como primeiro impulso utilizar o forEach com uma função async dentro dele para poderem utilizar o await e simplificarem o código, e isso - por diversas vezes - causa resultados inesperados como a ordem das requisições estar errada, ou de não conseguir pegar os dados de volta para retornar de uma função.
Caso de estudo
Então para podermos visualizar melhor esses casos, o exemplo de hoje será uma função que pega os resultados da [PokeAPI][https://pokeapi.co/docs/v2#info] e retorna os dados detalhados dos Pokemons nessa listagem. O endpoint que nós vamos usar é esse daqui:
https://pokeapi.co/api/v2/pokemon/?limit=limit&offset=offset
Então para começarmos, vamos fazer uma versão inicial dessa função ser assim:
async function listPokemons({ limit = 20, offset = 0 }) {
const endpoint = `https://pokeapi.co/api/v2/pokemon/?limit=${limit}&offset=${offset}`;
return fetch(endpoint)
.then((response) => response.json())
.then((data) => data.results);
}
async function app() {
const pokemons = await listPokemons({ limit: 5 });
console.log("Pokemons encontrados:");
console.log(pokemons);
}
app();
E o resultado da execução é esse daqui:
Pokemons encontrados:
[
{ name: 'bulbasaur', url: 'https://pokeapi.co/api/v2/pokemon/1/' },
{ name: 'ivysaur', url: 'https://pokeapi.co/api/v2/pokemon/2/' },
{ name: 'venusaur', url: 'https://pokeapi.co/api/v2/pokemon/3/' },
{ name: 'charmander', url: 'https://pokeapi.co/api/v2/pokemon/4/' },
{ name: 'charmeleon', url: 'https://pokeapi.co/api/v2/pokemon/5/' }
]
Como você pode perceber, nós só temos os nomes dos Pokemons, se nós quisermos ter as informações completas, nós precisariamos fazer uma requisição para cada url que ele volta para gente. O problema é como?
Versão 01 - forEach + função async
Voltando a nossa função listPokemons
nós poderíamos ter algo como:
const getJson = (url) => fetch(url).then((response) => response.json());
const PokeApi = {
baseUrl: "https://pokeapi.co/api/v2",
endpoint(part) {
return this.baseUrl.concat(part);
},
async list({ limit = 20, offset = 0 }) {
const url = this.endpoint(`/pokemon/?limit=${limit}&offset=${offset}`);
return await getJson(url)
.then((data) => data.results)
.catch(() => []);
},
};
async function listPokemons({ limit = 20, offset = 0 }) {
let pokemons = [];
const results = await PokeApi.list({ limit, offset });
results.forEach(async ({ url }) => {
const pokemon = await getJson(url);
pokemons.push(pokemon);
});
return pokemons;
}
async function app() {
const pokemons = await listPokemons({ limit: 5 });
console.log("Pokemons encontrados:");
console.log(pokemons);
}
app();
E o resultado vai ser:
Pokemons encontrados:
[]
Você deve estar se perguntando: Ué?? Mas ele não deveria ter voltado um array com os dados? Porque ele voltou vazio?
E a resposta para isso é simples, o forEach executa o await
na função interna dele, porém esse await
não tem efeito na nossa função listPokemons
afinal ele é o await
da função async que nós passamos como parâmetro do forEach
.
Assim toda vez que o forEach
executa uma iteração, ele cria uma nova Promise que vai ser resolvida no futuro, e só quando ela for resolvida que o dado vai ser adicionado ao array que nós iamos retornar na listPokemos
, então ele itera sobre todos os itens, cria Promises para todos eles, e antes que essas Promises se resolvam, ele continua executando o código do listPokemons
, pois como eu disse antes o await não surte efeito no fluxo princípal só no fluxo da callback do forEach
, e por ele continuar essa execução e as Promises não terem se resolvido, o array retornado ainda não tem nenhum item e é retornado em seu estado inicial: vazio.
Então o await
deveria estar no forEach
para que se esperar toda a execução dele terminar e ai sim retornar os dados completos, porém não tem como dar await
no forEach
, pois precisamos que a coisa que nós vamos dar o await
seja uma Promise
ou um objeto com o método then
, então como nós poderíamos converter a execução do forEach
em uma Promise que contém os resultados de todas as requisições?
Versão 02 - map + Promise.all
Nós podemos usar uma função do objeto Promise
do JS que é a função all
que é uma função que recebe um array de Promises, e espera que todas elas sejam executadas para converter os resultados delas em um array, que é exatamente o que a gente precisa.
Então para isso temos que mudar o nosso código, e utilizar o método map
ao invés do forEach
, dessa forma nós podemos converter o array de resultados em um array de promises desses resultados, e depois chamar o Promise.all
em cima disso:
async function listPokemons({ limit = 20, offset = 0 }) {
const results = await PokeApi.list({ limit, offset });
return Promise.all(results.map(({ url }) => getJson(url)));
}
E finalmente temos o resultado esperado:
Pokemons encontrados:
[
{
abilities: [ [Object], [Object] ],
base_experience: 64,
forms: [ [Object] ],
game_indices: [
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object]
],
height: 7,
held_items: [],
id: 1,
is_default: true,
location_area_encounters: 'https://pokeapi.co/api/v2/pokemon/1/encounters',
moves: [
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object]
],
name: 'bulbasaur',
order: 1,
past_types: [],
species: {
name: 'bulbasaur',
url: 'https://pokeapi.co/api/v2/pokemon-species/1/'
},
sprites: {
back_default: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/1.png',
back_female: null,
back_shiny: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/shiny/1.png',
back_shiny_female: null,
front_default: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png',
front_female: null,
front_shiny: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/1.png',
front_shiny_female: null,
other: [Object],
versions: [Object]
},
stats: [ [Object], [Object], [Object], [Object], [Object], [Object] ],
types: [ [Object], [Object] ],
weight: 69
},
{
abilities: [ [Object], [Object] ],
base_experience: 142,
forms: [ [Object] ],
game_indices: [
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object]
],
height: 10,
held_items: [],
id: 2,
is_default: true,
location_area_encounters: 'https://pokeapi.co/api/v2/pokemon/2/encounters',
moves: [
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object]
],
name: 'ivysaur',
order: 2,
past_types: [],
species: {
name: 'ivysaur',
url: 'https://pokeapi.co/api/v2/pokemon-species/2/'
},
sprites: {
back_default: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/2.png',
back_female: null,
back_shiny: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/shiny/2.png',
back_shiny_female: null,
front_default: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/2.png',
front_female: null,
front_shiny: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/2.png',
front_shiny_female: null,
other: [Object],
versions: [Object]
},
stats: [ [Object], [Object], [Object], [Object], [Object], [Object] ],
types: [ [Object], [Object] ],
weight: 130
},
{
abilities: [ [Object], [Object] ],
base_experience: 263,
forms: [ [Object] ],
game_indices: [
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object]
],
height: 20,
held_items: [],
id: 3,
is_default: true,
location_area_encounters: 'https://pokeapi.co/api/v2/pokemon/3/encounters',
moves: [
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object]
],
name: 'venusaur',
order: 3,
past_types: [],
species: {
name: 'venusaur',
url: 'https://pokeapi.co/api/v2/pokemon-species/3/'
},
sprites: {
back_default: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/3.png',
back_female: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/female/3.png',
back_shiny: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/shiny/3.png',
back_shiny_female: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/shiny/female/3.png',
front_default: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/3.png',
front_female: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/female/3.png',
front_shiny: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/3.png',
front_shiny_female: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/female/3.png',
other: [Object],
versions: [Object]
},
stats: [ [Object], [Object], [Object], [Object], [Object], [Object] ],
types: [ [Object], [Object] ],
weight: 1000
},
{
abilities: [ [Object], [Object] ],
base_experience: 62,
forms: [ [Object] ],
game_indices: [
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object]
],
height: 6,
held_items: [],
id: 4,
is_default: true,
location_area_encounters: 'https://pokeapi.co/api/v2/pokemon/4/encounters',
moves: [
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object],
... 2 more items
],
name: 'charmander',
order: 5,
past_types: [],
species: {
name: 'charmander',
url: 'https://pokeapi.co/api/v2/pokemon-species/4/'
},
sprites: {
back_default: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/4.png',
back_female: null,
back_shiny: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/shiny/4.png',
back_shiny_female: null,
front_default: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/4.png',
front_female: null,
front_shiny: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/4.png',
front_shiny_female: null,
other: [Object],
versions: [Object]
},
stats: [ [Object], [Object], [Object], [Object], [Object], [Object] ],
types: [ [Object] ],
weight: 85
},
{
abilities: [ [Object], [Object] ],
base_experience: 142,
forms: [ [Object] ],
game_indices: [
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object]
],
height: 11,
held_items: [],
id: 5,
is_default: true,
location_area_encounters: 'https://pokeapi.co/api/v2/pokemon/5/encounters',
moves: [
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object]
],
name: 'charmeleon',
order: 6,
past_types: [],
species: {
name: 'charmeleon',
url: 'https://pokeapi.co/api/v2/pokemon-species/5/'
},
sprites: {
back_default: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/5.png',
back_female: null,
back_shiny: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/shiny/5.png',
back_shiny_female: null,
front_default: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/5.png',
front_female: null,
front_shiny: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/5.png',
front_shiny_female: null,
other: [Object],
versions: [Object]
},
stats: [ [Object], [Object], [Object], [Object], [Object], [Object] ],
types: [ [Object] ],
weight: 190
}
]
Versão 3 - Tornando a execução serial com for/for await
O nosso código atual funciona como o esperado, porém ele executa de forma paralela, o que significa que todas as Promises executam ao mesmo tempo, o que é ótimo em termos de performance, ao invés de fazer uma tarefa por vez, nós fazemos todas de uma vez só.
Porém existem casos onde queremos que as Promises executem de forma serial, ou seja, se nós temos um array de Promises, e queremos que cada Promise só seja executada depois que a anterior finalizou a sua execução, mesmo que isso não nos dê mais os benefícios de performance de uma execução paralela, pode ser necessário para se assegurar que os dados sejam cadastrados na ordem correta em alguns casos - por exemplo em uma API que requere que você cadastre um usuário em um endpoint, e depois cadastre as fotos dele em outro endpoint, e para que a segunda seja feita com sucesso, a primeira precisa ter sido feita antes, afinal não dá para cadastrar fotos em um usuário que não existe.
Então como bônus vamos ver como poderíamos tornar a execução da nossa função listPokemons
como se ela fosse síncrona novamente (apesar da execução do combo map
+ Promise.all
ser melhor nesse nosso exemplo em específico):
async function listPokemons({ limit = 20, offset = 0 }) {
const pokemons = [];
const results = await PokeApi.list({ limit, offset });
for (const result of results) {
pokemons.push(await getJson(result.url));
}
return pokemons;
}
Ou se utilizarmos uma sintaxe mais moderna por meio do for await of:
async function listPokemons({ limit = 20, offset = 0 }) {
const pokemons = [];
const details = await PokeApi.list({ limit, offset })
.then((results) => results.map(({ url }) => url))
.then((urls) => urls.map(getJson));
for await (const pokemon of details) pokemons.push(pokemon);
return pokemons;
}
Conclusão
Eai conhecia essas peculiaridades sobre a interação entre Promises e arrays? Comente ai se o artigo te agregou alguma coisa, e até mais, te esperamos no próximo artigo.
~ Suporte Cansado
Top comments (0)