Chrome Dev Summit is back! Visit goo.gle/cds2021 to secure your spot in workshops, office hours and learning lounges!

Working with the Fetch API

Codelab: Fetch API

What is fetch?

The Fetch API is a simple interface for fetching resources. Fetch makes it easier to make web requests and handle responses than with the older XMLHttpRequest, which often requires additional logic (for example, for handling redirects).

You can check for browser support of fetch in the window interface. For example:

main.js

if (!('fetch' in window)) {
  console.log('Fetch API not found, please upgrade your browser.');
  return;
}
// We can safely use fetch from now on

Fetch is supported across all modern browsers, but there is a polyfill if you need to support older browsers.

The fetch() method takes the path to a resource as input. The method returns a promise that resolves to the Response of that request.

Making a request

Let's look at a simple example of fetching a JSON file:

main.js (with promise chaining)

fetch('examples/example.json')
.then(function(response) {
  // Do stuff with the response
})
.catch(function(error) {
  console.log('Looks like there was a problem: ', error);
});

We pass the path for the resource we want to retrieve as a parameter to fetch. In this example, this is examples/example.json. The fetch call returns a promise that resolves to a response object.

When the promise resolves, the response is passed to .then. This is where the response could be used. If the request does not complete, .catch takes over and is passed the corresponding error.

Note, the previous example uses promise chaining, however async/await can simplify your code. The remaining examples use async/await or top-level await.

Here is the same example as before, but converted to use top-level await.

main.js (with top-level await)

try {
  const response = await fetch('examples/example.json');
} catch (error) {
  console.log('Looks like there was a problem: ', error);
}

Response objects represent the response to a request. They contain the requested resource and useful properties and methods. For example, response.ok, response.status, and response.statusText can all be used to evaluate the status of the response.

Evaluating the success of responses is particularly important when using fetch because bad responses (like 404s) still resolve. The only time a fetch promise will reject is if the request was unable to complete. The previous code segment would only error if there was no network connection, but not if the response was bad (like a 404). If the previous code were updated to validate responses it would look like:

main.js

try {
  const response = await fetch('examples/example.json');
  if (!response.ok) {
    throw Error(`${response.status} ${response.statusText}`);
  }
} catch (error) {
  console.log('Looks like there was a problem: ', error);
}

Now if the response object's ok property is false (indicating a non 200-299 response), the function throws an error containing response.status and response.statusText that is caught in the catch-block. This ensures that bad responses are caught early.

Reading the response object

Responses must be read in order to access the body of the response. Response objects have methods for doing this. For example, Response.json() reads the response and returns a promise that resolves to JSON. Adding this step to the current example updates the code to:

main.js

try {
  const response = await fetch('examples/example.json');
  if (!response.ok) {
    throw Error(`${response.status} ${response.statusText}`);
  }
  // Read the response as json.
  const json = await response.json();
} catch (error) {
  console.log('Looks like there was a problem: ', error);
}

You can wrap the previous code into a function and use it as follows:

main.js

async function fetchResource(pathToResource) {
  try {
    const response = await fetch(pathToResource);
    if (!response.ok) {
      throw Error(`${response.status} ${response.statusText}`);
    }
    return response;
  } catch (error) {
    console.log('Looks like there was a problem: ', error);
  }
}

const response = await fetchResource('examples/example.json');
if (response) {
  // Read the response as json.
  console.log(await response.json())
}

For more information

Example: fetching images

Let's look at an example of fetching an image and appending it to a web page.

main.js

function showImage(responseAsBlob) {
  const imgUrl = URL.createObjectURL(responseAsBlob);
  const imageEl = document.createElement('img');
  imageEl.src = imgUrl;
  document.body.appendChild(imageEl);
}

// Uses the same fetchResource function as shown in previous examples
const response = await fetchResource('examples/kitten.jpg');
if (response) {
  showImage(await response.blob());
}

In this example an image (examples/kitten.jpg) is fetched. Similar to the previous example, the response is validated with the fetchResource function. The response is then read as a Blob (instead of as JSON), and an image element is created and appended to the page, and the image's src attribute is set to a data URL representing the Blob.

For more information

Example: fetching text

Let's look at another example, this time fetching some text and inserting it into the current page.

main.js

function showText(responseAsText) {
  document.body.textContent = responseAsText;
}

const response = await fetchResource('examples/words.txt');
if (response) {
  showText(await response.text());
}

In this example a text file is being fetched, examples/words.txt. Like the previous two examples, the response is validated with the fetchResource function. Then the response is read as text, and appended to the page.

For more information

Making custom requests

fetch() can also receive a second optional parameter, init, that allows you to create custom settings for the request, such as the request method, cache mode, credentials, and more.

Example: HEAD requests

By default fetch uses the GET method, which retrieves a specific resource, but other request HTTP methods can also be used.

HEAD requests are just like GET requests except the body of the response is empty. You can use this kind of request when all you want is the file's metadata, and you want or need the file's data to be transported.

To call an API with a HEAD request, set the method in the init parameter. For example:

main.js

fetch('examples/words.txt', {
  method: 'HEAD'
})

This will make a HEAD request for examples/words.txt. We can update the fetchResource function signature to receive an options object for the fetch API.

main.js

async function fetchResource(pathToResource, init) {
  try {
    // Pass `init` to fetch()
    const response = await fetch(pathToResource, init);
    if (!response.ok) {
      throw Error(`${response.status} ${response.statusText}`);
    }
    return response;
  } catch (error) {
    console.log('Looks like there was a problem: ', error);
  }
}

Now, you could use a HEAD request to check the size of a resource. For example:

main.js

const response = await fetchResource('examples/words.txt', {
  method: 'HEAD'
});

if (response) {
  const size = response.headers.get('content-length');
}

Here the HEAD method is used to request the size (in bytes) of a resource (represented in the content-length header) without actually loading the resource itself. In practice this could be used to determine if the full resource should be requested (or even how to request it).

Example: POST requests

Fetch can also send data to an API with POST requests. The following code sends a "title" and "message" (as a string) to someurl/comment:

main.js

fetch('someurl/comment', {
  method: 'POST',
  body: 'title=hello&message=world'
})

The method is again specified with the init parameter. This is also where the body of the request is set, which represents the data to be sent (in this case the title and message).

The body data could also be extracted from a form using the FormData interface. For example, the above code could be updated to:

main.js

// Assuming an HTML <form> with id of 'myForm'
fetch('someurl/comment', {
  method: 'POST',
  body: new FormData(document.getElementById('myForm')
})

Custom headers

The init parameter can be used with the Headers interface to perform various actions on HTTP request and response headers, including retrieving, setting, adding, and removing them. An example of reading response headers was shown in a previous section. The following code demonstrates how a custom Headers object can be created and used with a fetch request:

main.js

const myHeaders = new Headers({
  'Content-Type': 'text/plain',
  'X-Custom-Header': 'hello world'
});

fetch('/someurl', {
  headers: myHeaders
});

Here we are creating a Headers object where the Content-Type header has the value of text/plain and a custom X-Custom-Header header has the value of hello world.

Custom headers on cross-origin requests must be supported by the server from which the resource is requested. The server in this example would need to be configured to accept the X-Custom-Header header in order for the fetch to succeed. When a custom header is set, the browser performs a preflight check. This means that the browser first sends an OPTIONS request to the server to determine what HTTP methods and headers are allowed by the server. If the server is configured to accept the method and headers of the original request, then it is sent. Otherwise, an error is thrown.

For more information

Cross-origin requests

Fetch (and XMLHttpRequest) follow the same-origin policy. This means that browsers restrict cross-origin HTTP requests from within scripts. A cross-origin request occurs when one domain (for example http://foo.com/) requests a resource from a separate domain (for example http://bar.com/). This code shows a simple example of a cross-origin request:

main.js

// From https://foo.com/
const response = await fetch('https://bar.com/data.json');
// do something with the response

There have been attempts to work around the same-origin policy (such as JSONP). The Cross Origin Resource Sharing (CORS) mechanism has enabled a standardized means of retrieving cross-origin resources. The CORS mechanism lets you specify in a request that you want to retrieve a cross-origin resource (in fetch this is enabled by default). The browser adds an Origin header to the request, and then requests the appropriate resource. The browser only returns the response if the server returns an Access-Control-Allow-Origin header specifying that the origin has permission to request the resource. In practice, servers that expect a variety of parties to request their resources (such as 3rd party APIs) set a wildcard value for the Access-Control-Allow-Origin header, allowing anyone to access that resource.

If the server you are requesting from doesn't support CORS, you should get an error in the console indicating that the cross-origin request is blocked due to the CORS Access-Control-Allow-Origin header being missing.

You can use no-cors mode to request opaque resources. Opaque responses can't be accessed with JavaScript but the response can still be served or cached by a service worker. Using no-cors mode with fetch is relatively simple. To update the above example with no-cors, we pass in the init object with mode set to no-cors:

main.js

// From https://foo.com/
const response = await fetch('https://bar.com/data.json', {
  mode: 'no-cors' // 'cors' by default
})
// Do something with response

For more information

Further reading