1

I have the following code:

  getContent() {

    fetch(this.url)
    .then((response) => response.text())
    .then((data) => {
      let jsonData = JSON.parse(data);

      for (let i=0; i<jsonData.results.length; i++) {

        this.fetchPokemon(jsonData.results[i].url)
        .then(() => console.log("end"));
      }

      console.log(calls);
    })
    .catch((error) => console.log(error));
  }

  fetchPokemon(url) {
    return new Promise((resolve) => {
      fetch(url)
      .then((response) => response.text())
      .then((data) => {
        let jsonData = JSON.parse(data);

        let types = new Array();
        for (let i=0; i<jsonData.types.length; i++) {
          types.push(jsonData.types[i].type.name);
        }

        this.pokemons.push(new Pokemon(jsonData.name, types, jsonData.weight, jsonData.sprites.front_default));
        resolve();
      })
      .catch((error) => console.log(error));
    });
  }

These are two functions in a JS class I'm working on. As you can see, function getContent() executes fetch() and gets some information from an API. These information contain more URLs where I'd like to get some information as well so I placed a for loop to execute the second function fetchPokemon(url) which will make another API call to get some other information.

I'm struggling to understand how can I detect from getContent() when all the Promises in the loop get resolved correctly so I can proceed with other functionalities. I feel like Promise.all can help, but I didn't manage to succeed.

How can I do this?

2 Answers2

2

promote reusability

Try not to put too much into your classes. Functions should be designed around portability, making them easier to resuse and test.

function getJson(url) {
 return fetch(url)
   .then((response) => response.text())
   .then((data) => JSON.parse(data))
}

Now the class can stay lean and any other part of your program that needs JSON can benefit from the separated function as well -

class MyClass {
  constructor () {
    // ...
  }
  async getContent () {
    const data = await getJson(this.url)
    // ...
  }
}

Moving onto fetchPokemon, look at that, we need some more JSON! Fetching a single Pokemon is a simple task that requires only an input url and returns a new Pokemon. There's no reason to tangle it with class logic -

async function fetchPokemon(url) {
  const { name, types, weight, sprites } = await getJson(url)
  return new Pokemon(
    name,
    types.map(t => t.name),
    weight,
    sprites.front_default
  )
}

Now we can finish the getContent method of our class -

class MyClass {
  constructor () {
    // ...
  }
  async getContent () {
    const { results } = await getJson(this.url)
    return Promise.all(results.map(item => fetchPokemon(item.url)))
  }
}

veil lifted

Now we can plainly see that getContent is nothing more than a simple function that requests multiple pokemon. If it were mere, I would also make this a generic function -

async function fetchAllPokemon(url) {
  const { results } = await getJson(url)
  return Promise.all(results.map(item => fetchPokemon(item.url)))
}

The need for your class has essentially vanished. You haven't shared the other parts of it, but your program has simplified to three (3) functions getJson, fetchPokemon and fetchAllPokemon all of which are reusable and easy to test, and a dead-simple MyClass wrapper -

function getJson(...) { ... }
async function fetchPokemon(...) { ... }
async function fetchAllPokemon(...) { ... }

class MyClass {
  constructor() {
    // ...
  }
  getContent() {
    return fetchAllPokemon(this.url)
  }
}

promote versatility

I should also mention that getJson can be simplified, as the fetch response includes a .json method. To make it even more useful, you can add a second options parameter -

function getJson(url, options = {}) {
  return fetch(url, options).then(res => res.json())
}

don't swallow errors

Finally don't attach .catch error handlers on your generic functions or class methods. Errors will automatically bubble up and reserves opportunity for the caller to do something more meaningful -

fetchAllPokemon(someUrl)
  .then(displayResult)
  .catch(handleError)

Or if you still plan on using a class -

const foobar = new MyClass(...)
foobar
  .getContent()
  .then(displayResult)
  .catch(handlerError)

zoom out

Historically classes were used to organize your program by encapsulating logic, variables, and methods. Often times class structures result in hard-to-manage and difficult-to-test hierarchies. Nowadays with module support via import and export, you can write programs that are "flat" and functions and components can be combined in flexible ways. Consider this ExpressJS example using the functions we wrote above -

import express from "express"
import { fetchPokemon, fetchAllPokemon } from "./api.js"

app.get("/pokemon", (res, res, next) => {
  fetchAllPokemon(`https://someurl/`)
    .then(data => res.json(data))
    .then(_ => next())
    .catch(err => next(err))
}

app.get("/pokemon/:id",(req,res,next)=> {
  fetchPokemon(`https://someurl/${req.params.id}`)
    .then(data => res.json(data))
    .then(_ => next())
    .catch(err => next(err))
}

Or even better with async and await, as errors automatically bubble up -

import express from "express"
import { fetchPokemon, fetchAllPokemon } from "./api.js"

app.get("/pokemon", async (res, res) => {
  const data = await fetchAllPokemon(`https://someurl/`)
  res.json(data)
}

app.get("/pokemon/:id", async (res, res) => {
  const data = await fetchPokemon(`https://someurl/${req.params.id}`)
  res.json(data)
}
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • Love this answer, except minimizing the use of classes, one of the most valuable inventions in computing, strikes me as an odd choice. I think better advice is: yes, small classes, with focussed purpose; but the function we cut in order to bring focus belongs in *other* small, focussed classes. Applying that idea here, maybe `getJson()` is a method on a remote service that maintains state like auth state or cached results. – danh Jul 03 '21 at 17:14
  • @danh thanks so much for your comment :D there are certain schools of thought that would argue classes (as implemented in popular languages like JavaScript, Python, Java, C#, C++) are big misstep in the pursuit of writing good software. Managing state and hierarchies is the source of many bugs and countless developer headaches and wasted hours. This is not to say "classes are bad" but that they are often misunderstood and misapplied. I have written extensively on the topic and invite you to review [this Q&A](https://stackoverflow.com/a/66340109/633183) for a detailed write up on the topic. – Mulan Jul 03 '21 at 17:24
  • Other "proof" that modules are the way forward in JS is we are seeing increased support around `import` and `export`, as well as supporting asynchronous module loading. And module-based designs are [tree-shakeable](https://en.wikipedia.org/wiki/Tree_shaking), meaning any part of a module that is not used can be removed from a "compiled" program. This is simply not possible with class-oriented designs as dynamic state and context are all interdependent. Google's [Firebase v9 SDK](https://firebase.google.com/docs/web/learn-more#modular-version) is a great example of this. – Mulan Jul 03 '21 at 17:29
  • Thanks, @Mulan. I'll do more reading on this subject, beginning with what you've suggested. – danh Jul 03 '21 at 19:02
1

Try using Promise.all like this:

  getContent() {
    fetch(this.url)
    .then((response) => response.text())
    .then((data) => {
      let jsonData = JSON.parse(data);

      const promises = [];
      for (let i=0; i<jsonData.results.length; i++) {

        promises.push(this.fetchPokemon(jsonData.results[i].url));
      }

      return Promise.all(promises);
    })
    .then((results) => {
      console.log(results);
    })
    .catch((error) => console.log(error));
  }

So basically you just push all of the Promises to the array and then apply Promise.all to wait for them to resolve.

Sebastian Kaczmarek
  • 8,120
  • 4
  • 20
  • 38