Como lidar com múltiplas Promises no JS
Tags: braziliandevs, javascript
Published: 2023-02-23 22:11:28

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:

```js
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:
```js
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:
```js
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):
```js
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:
```js
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