Asynchronous Programming: The Fetch API And Promises

Asynchronous Programming: The Fetch API And Promises

The Fetch API provides an interface to fetch resources across a network. Anyone who has used the XMLHttpRequest method will be familiar with the callback hell or event listening that is associated with this method. Initially, we use the XMLHttpRequest with callbacks or listen to the load event it emits when it's done.

const request = new XMLHttpRequest();
request.open('GET', url);
request.send();

afterward, we proceed to do this which was initially fine

request.onreadystatechange = function () {
  if (request.readyState == 4 && request.status == 200) {
    console.log(JSON.parse(request.responseText));
  }
};

Or

request.addEventListener('load', function () {
  if (request.readyState == 4 && request.status == 200) {
    const response = JSON.parse(request.responseText);
    console.log(response);
  }
});

This seems ok for now but what if we wanted to do several requests at the same time where the order is a high priority. Then we would have to embed several callbacks in callbacks resulting in something we call callback hell which affects performance.

All these can be avoided with the fetch API. The API accepts one mandatory value fetch(url) for a simple fetch call and returns a promise which resolves to a response as soon as the server responds with Headers. The promise still resolves even if the server responds with an HTTP (Hypertext Transfer Protocol) error status. The fetch will only reject if there is a network failure or something interrupted the process. Now, what is a promise?

A promise is an object that is used as a placeholder for future responses to asynchronous activity.

Look at it like a lottery, you don't get your response immediately but rather they give a promise that if you win they will give you this sum of money or something. The promise returned by the API works the same way. It does not resolve immediately because promises can be rejected or fulfilled.

A promise undergoes some life cycle activities before a response is returned. The initial state at the point where the promise is created by the fetch is pending. Afterward, it moves to the settled state. This is when the promise is either rejected or fulfilled. A promise is fulfilled if we get the response we wanted from the request.

On the fetch API, we can call some other methods such as

  • then()
  • finally()
  • catch()

The then() method

The method accepts a value which is normally the promise returned by the fetch. It also can return a value. This property allows us to chain multiple then() methods together to make a simple fetch -request and response- call.

fetch(url)
  .then(response => response.json())
  .then(data => console.log(data));

Now from our above code, the fetch returns a promise which is passed to the then() immediately called on it. Like we said the promise returned is an object so it has some methods. The then() method also returns the response body (the response we expected) in a JSON format which is then received by the following then() method into data.

Imagine we wanted to make another request but it should be after the previous has been resolved. Then we could have done it like this with the fetch API

fetch(url)
  .then(response => response.json())
  .then(data => {
    console.log(data);
    fetch(url)
      .then(response => response.json())
      .then(data => {
        console.log(data);
      });
  });

But we would be implementing the same callback hell we are trying to avoid. But remember the then() also returns a value that can be a promise. We can leverage this to our advantage. Instead of the previous, we can do something like this

fetch(url)
  .then(response => response.json())
  .then(data => {
    console.log(data);
    return fetch(url);
  })
 .then(response => response.json())
 .then(data => {
  console.log(data);
 });

We can chain as many requests with this approach without worrying about nested callbacks.

The finally() method

This method not mostly used, maybe to me. But it's use if there is something that should happen whether the request resolves or rejects. So to say it will run or execute no matter what happens.

fetch(url)
  .then(response => response.json())
  .then(data => {
    console.log(data);
  })
 .finally( () => {
    console.log('hello there');
  })

The catch() method

One of the most important aspects of any application is error handling. The catch method is use to catch any error that may occur during the request. But it cannot catch reject or an HTTP error status. Because basically, those are not errors. They are just responses from the server.

fetch(url)
  .then(response => response.json())
  .then(data => {
    console.log(data);
  })
 .catch( err => {
    console.log(err);
  })

To be able to catch an error if the request is rejected, then we have to check for that state and throw an error ourselves.

fetch(url)
  .then(response => {
    if( !response.ok ) throw new Error('something happened')
    return response.json();
  })
  .then(data => {
    console.log(data);
  })
 .catch(err => {
    console.log(err);
  })

We conclude with some importance of the fetch API and promises

  • With the API and promises there is no need to rely on callbacks and events.
  • Also we can chain promises for a sequence of asynchronous request.
ย