Fetching Data with the Fetch API

In this module we’ll explore using the Fetch API to download data directly from an URL or an API.

Table of Contents

  1. But You Promised!
  2. And Then What?
  3. The Fetch API
  4. Fetching JSON
  5. GET Request
  6. POST Requests
  7. POST with JSON
  8. Handling Errors
  9. Then Chains
  10. Async / Await
  11. Further Reading

But You Promised!

⏳ Wait For It:

Before we can talk about fetching data, a word on Promises.

Promises are a recent addition to Javascript that represent the eventual result of an asynchronous operation.

You ask for something and you get back a promise for that thing.

A promise can be in one of four states:

  • fulfilled - Result is available.
  • rejected - Failed to retrieve result.
  • pending - Not yet fulfilled or rejected.
  • settled - Fulfilled or rejected.

And Then What?

We use .then() to register a callback to handle fulfilled promises. This callback will be called once the promise has been fulfilled.

We can chain .then()s together to link together a series of async tasks.

We can also register a callback to handle rejected promises using .catch().

All of this will make more sense with an example. So let’s dive into fetching data using Javascript’s Fetch API.

The Fetch API

The Fetch API makes available a global fetch() function that we can use to request data from a URL using HTTP. This function returns a Promise we can use to eventually access the fetched data.

fetch("https://dog.ceo/api/breeds/list/all").then(function (result) {
  console.log("HTTP Response Status: " + result.status);
});

This request will print to the console once the Promise is fulfilled:

HTTP Response Status: 200

Fetching JSON

If the data being fetched is JSON we can request another Promise for the de-serialized Javascript version of the JSON string.

fetch("https://dog.ceo/api/breeds/list/all")
  .then(function (result) {
    return result.json(); // Promise for parsed JSON.
  })
  .then(function (data) {
    // Executed when promised JSON is ready.
    let breeds = Object.keys(data.message);
    for (let breed of breeds) {
      console.log(breed);
    }
  });

The reasons we set breeds to Object.keys(data.message) is because the API returns:

{
  "status": "success",
  "message": {
    "affenpinscher": [],
    // skip a few
    "setter": [
      "english",
      "gordon",
      "irish"
    ],
    // skip a few more
    "wolfhound": [
      "irish"
    ]
  }
}

And for this example we only care about the keys of the message property.

GET Request

Here’s another example of an HTTP GET request for some JSON:

fetch("https://www.reddit.com/r/javascript/top/.json?limit=5")
  .then(function (result) {
    return result.json(); // Promise for parsed JSON.
  })
  .then(function (retrieved) {
    let articles = retrieved.data.children;
    for (let article of articles) {
      console.log(article.data.title);
    }
  });

The relevant keys of the JSON returned by the Reddit API look like this:

{
  "data": {
    "children": [
      {
        "data": {
          "title": "Title of the first article"
        }
      },
      {
        "data": {
          "title": "Title of the second article"
        }
      }
      // etc
    ]
  }
}

POST Requests

If you wanted to use Javascript to asynchronously submit an HTML form:

fetch("https://example.com/endpoint", {
  method: "post",
  body: new FormData(document.querySelector("form")),
}).then(function (response) {
  if (!response.ok) {
    console.log("POST failed. Status Code:  " + response.status);
  }
});

This will submit the data present in the first form on the page.

POST with JSON

Some APIs will have endpoints that you can POST JSON data to:

fetch("https://jsonplaceholder.typicode.com/posts", {
  method: "POST",
  body: JSON.stringify({ title: "foo", body: "bar", userId: 1 }),
  headers: {
    "Content-type": "application/json; charset=UTF-8",
  },
})
  .then((response) => response.json())
  .then((json) => console.log(json));

Note the use of the fat arrow functions in the thens.

Handling Errors

Errors can be handled by chaining a .catch to your fetch, but this will only catch network errors. It won’t catch bad HTTP status responses like 404 or 500.

To handle cases where the network request succeeded but the HTTP status code was bad:

function handleErrors(response) {
  if (!response.ok) {
    throw Error(response.statusText);
  }
  return response;
}

fetch("http://httpstat.us/500")
  .then(handleErrors) // Check for HTTP status failure.
  .then(function(response) { // Handle success. })
  .catch(function(error) { // Handle network or HTTP failure. })

Then Chains

When working with promises we sometimes end up with a chain of .then() calls to resolve multiple nested asynchronous callbacks. These can be hard to read and reason about.

The dog breed API example from above generates two promises, one for the fetch and another for parsing the returned JSON string:

fetch("https://dog.ceo/api/breeds/list/all") // Fetch returns a promise.
  .then(function (result) {
    // Then for promised fetch result.
    return result.json(); // JSON parsing also returns a promise.
  })
  .then(function (data) {
    // Then for promised JSON parsing.
    let breeds = Object.keys(data.message);
    for (let breed of breeds) {
      console.log(breed);
    }
  });

Async / Await

Let’s move the code from the last example into a logAllBreeds function. If we mark this function as async we can use the await keyword to write code that doesn’t rely on callbacks:

async function logAllBreeds() {
  let results = await fetch("https://dog.ceo/api/breeds/list/all");
  let data = await results.json();

  let breeds = Object.keys(data.message);
  for (let breed of breeds) {
    console.log(breed);
  }
}

The async keyword ensures that a function always returns a promise.

The await keyword makes Javascript wait until a promise is settled before the code continues executing.

Further Reading