DEV Community

Terminal Coffee
Terminal Coffee

Posted on • Updated on

Como lidar com múltiplas Promises no JS

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
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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/' }
]
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

E o resultado vai ser:

Pokemons encontrados:
[]
Enter fullscreen mode Exit fullscreen mode

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)));
}
Enter fullscreen mode Exit fullscreen mode

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
  }
]
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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)