Guide to using the Fetch API in JavaScript

The Fetch API is the modern way to send and receive data in JavaScript.

The syntax for fetching some data is simple:

fetch('http://endpoint-url.com/api')

This immediately returns a promise that resolves to a Response object. This provides you with information about the status of the response and headers the data is read.

fetch('http://endpoint-url.com/api')
.then((res) => {
  console.log(res.status); // 200
  console.log(res.statusText); // OK
  console.log(res.headers); 
  // Headers { "content-length" → "456", "content-type" → "application/json" }
})

The data can be read from the Response object via a ReadableStream on its body property. This also returns a promise.

You can read it by calling a method on the Response object. The most common is res.json() to read data that is in JSON format to a JS object:

fetch('http://endpoint-url.com/api')
.then(res => res.json()) // Wait for Response object and read to JS object
.then(data => console.log(data)) // Wait for ReadableStream to be read
.catch(err => console.error(err)) // Runs if error occurs in previous steps

It’s also possible to read the data into a file container using res.blob() or string format with res.text().

An error is not throw automaticaly if you receive an non-success response code to your request. To add this, you can query the statusText and status (response code) properties on the Response object:

fetch('http://endpoint-url.com/api')
.then(res => {
    if (!res.statusText === "OK") {
        throw new Error(`Bad server response (${res.status})`);
    };
    return res.json();
})
.then(data => console.log(data))
.catch(err => console.log(err)); // Now runs if there is a bad response code

Sending data

You can make a POST request with Fetch by passing in a configuration object as a second argument when calling it.

Setting a value for a method property determines the type of request and the data to be sent is set on body:

const obj = { a: "1", b: "2", c: "3" };
const payload = JSON.stringify(obj);

fetch('http://endpoint-url.com/api', {
  method: "POST",
  body: payload,
  headers: {
    "Content-type": "application/json",
  }
}) 
.then(res => res.json())
.then(data => console.log(data))

If sending a form data payload created using the FormData() constructor, you do not need to set any headers as these will be set automatically. Doing so can cause an error.

With async/await

Because fetch() returns a promise (and so does reading the data from its Response object) its result can be handled using async/await syntax, using the await keyword to await the results of the promises.

Errors can be handled in a catch block that runs if the fetch request, made in a try block, fails:

async function getData(url) {
  try {
    const res = await fetch(url); // Wait for Repsonse object
    const data = await res.json(); // Wait for parsing of data
    console.log(data);
  } catch(err) {
    console.error(err); // Runs if there is an error in try block
  }
}

getData('http://endpoint-url.com/api');

Download progress

It is possible to track the download progress of a fetch request, though a little long-winded.

To do so, call getReader() on the body of the response to generate a new reader. Download progress can be obtained via calling read() on it and accessing its result, which is in an object returned in a promise every time a chunk of data is read.

At the same time, the data being read needs to be returned in a promise to the next .then() method. Therefore, this entire process is nested in the creation of a new Response object with a ReadableStream on it.

fetch('https://picsum.photos/400/400')
.then(res => {
  const contentLength = res.headers.get('content-length');
  let loaded = 0;
  
  // Create new Response object with ReadableStream:
  return new Response(
    new ReadableStream({
      start(controller) {
        // At the same time read body of original response with progress:
        const reader = res.body.getReader();
        (function read() {
          reader.read()
          .then((progressEvent) => {
            if (progressEvent.done) {
              controller.close();
              return; 
            }
            loaded += progressEvent.value.byteLength;
            console.log(Math.round(loaded/contentLength*100)+'%');
            // Push new read value into new ReadableStream:
            controller.enqueue(progressEvent.value);
            read();
          })
        })();
      }
    })
  );
})
.then(res => res.blob()) // Reads new ReadableStream
.then(blob => {
  const url = URL.createObjectURL(blob); // Creates URL to blob
  const img = new Image();
  img.src = url; // Sets as src of new image
  document.body.appendChild(img); // Inserts image in DOM
});