0

was following this tutorial(https://javascript.info/fetch) on javascript's promise and async await and had trouble understanding the exercise it provided.

The question is about retrieving multiple users info from Github. One fetch request per user. And requests shouldn’t wait for each other. So that the data arrives as soon as possible.

The solution it provided is

async function getUsers(names) {
  let jobs = [];

  for(let name of names) {
    let job = fetch(`https://api.github.com/users/${name}`).then(
      successResponse => {
        if (successResponse.status != 200) {
          return null;
        } else {
          return successResponse.json();
        }
      },
      failResponse => {
        return null;
      }
    );
    jobs.push(job);
  }

  let results = await Promise.all(jobs);

  return results;
}

My first question is, can we use await for the fetch. i.e. is the following snippet equivalent to the solution he provided?

async function getUsers2(names) {
  let jobs = [];

  for(let name of names) {
    let response
    try {
      response = await fetch(`https://api.github.com/users/${name}`);
    } catch(e) {
      response = null
    }
    const job = response && response.json()
    jobs.push(job);
  }

  let results = await Promise.all(jobs);

  return results;
} 

Furthermore, the tutorial said

.then call is attached directly to fetch, so that when we have the response, it doesn’t wait for other fetches, but starts to read .json() immediately.

If we used await Promise.all(names.map(name => fetch(...))), and call .json() on the results, then it would wait for all fetches to respond. By adding .json() directly to each fetch, we ensure that individual fetches start reading data as JSON without waiting for each other.

Does he mean that if we write the solution this way

async function getUser(name) {
  const response = await fetch(`https://api.github.com/users/${name}`)

  return response.ok ? await response.json : null
}

async function getUsers(names) {
  return await Promise.all(names.map(name => getUser(name)))
}

we wouldn't be able to achieve the effect such that we don't want requests shouldn’t wait for each other?

Community
  • 1
  • 1
Joji
  • 4,703
  • 7
  • 41
  • 86
  • The second solution using `await` in front of all fetches is not equivalent to the first one. `await`... waits, so your fetches are not executed in parralel as in the first example – blex Mar 01 '20 at 23:02

1 Answers1

3

My first question is, can we use await for the fetch. i.e. is the following snippet equivalent to the solution he provided?

No. When in the immediate body of an async function, whenever there's an await, the function will completely pause until the following Promise resolves. So, the loop

  for(let name of names) {
    let response
    try {
      response = await fetch(`https://api.github.com/users/${name}`);
    } catch(e) {
      response = null
    }

needs to wait in serial for the headers of each response to be received before continuing on to initialize the next request.

Does he mean that if we write the solution this way

First, the syntax needs to be adjusted: .json is a method, so it needs to be called:

async function getUser(name) {
  const response = await fetch(`https://api.github.com/users/${name}`)
  return response.ok ? await response.json() : null
  //                                      ^^
}

But that's perfectly fine to do. The only await in the getUsers function is waiting for the whole Promise.all to resolve; the .mapping of the array to the getUser call is carried out synchronously, so all requests get sent out at once, so none of the network requests need to wait for any of the others to finish in order to work.

The problem the author was referring to was calling Promise.all on an array of the fetch calls, rather than on an array of the .json() calls:

// Bad, do not use:
const getUsers = async (names) => {
  const responses = await Promise.all(names.map(
    name => fetch(`https://api.github.com/users/${name}`)
  ));
  return Promise.all(responses.map(
    response => response.ok ? response.json() : null
  ));
}

The problem with the above is that the script must wait for all of the response headers from every request to be received before the response body for any of them can start to be parsed.

Another parallel solution:

const getUsers = names => Promise.all(names.map(
  async (name) => {
    const res = await fetch(`https://api.github.com/users/${name}`);
    return res.ok ? res.json() : null;
  }
));
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • +1, although, a little nuance regarding the last question _"Does he mean that if we write"_, the answer is _no_, the author does not mean that. He means that waiting for `Promise.all` and only then doing `response.json()` would be a waste of time. The way OP wrote it is fine (except for what you mentioned). – blex Mar 01 '20 at 23:08
  • @CertainPerformance Hi thanks for the reply. So my first approach, the one I used try catch actually makes every fetch to wait for the previous fetch to complete? so it would be as bad as the one you mentioned with calling `Promise.all` on an array of the fetch calls? – Joji Mar 02 '20 at 03:18
  • By the way I am curious that the solution the tutorial author provided, it returns two null separately. I wonder if that's necessary and when will it return the second `null`. I tried to mess up the url but it ended up returning the first `null` – Joji Mar 02 '20 at 03:21
  • @Joji *so it would be as bad as the one you mentioned with calling Promise.all on an array of the fetch calls?* Significantly worse, actually - the bad `Promise.all` version waits for *all* of its Promise requests to resolve in parallel, but all requests get sent out immediately. `await`ing each `fetch` call, on the other hand, only sends out a request after the headers from the last request have been received. – CertainPerformance Mar 02 '20 at 03:38
  • @Joji Oh, missed that. Yeah, that's for if the `fetch` rejects *immediately* (not common, it's the sort of thing that'll throw if the internet is disconnected). You can take it into account by adding a `catch` onto the end of the `fetch` call. But mostly ignoring errors and just putting `null` in their place is weird, I'd prefer something like [`Promise.allSettled`](https://stackoverflow.com/a/56255129) – CertainPerformance Mar 02 '20 at 03:42
  • @CertainPerformance Hi. This is a late reply but I was wondering for the solution where getUser and async/await is used, how do we handle the error when the network is disconnected. Right now in that solution, we only check `response.ok ? await response.json() : null` this only accounts for when the successResponse.status is not 200. How do we add an extra error handling for the case where the network is not working? – Joji Mar 08 '20 at 22:16
  • Wrap the `fetch` in a separate `.catch` and if the `catch` is entered, there's been an error. `await fetch(...).catch(err => 'Network err')` But all this error handling in the fetching function is strange, I'd much prefer to handle it outside, in the consumer – CertainPerformance Mar 08 '20 at 22:30