Network Requests Without Callbacks

The fetch() API handles network requests the way XMLHttpRequest (XHR) does, but with a Promise-based interface instead of the callback-heavy approach you get with XHR. That makes typical request code shorter and avoids nested callbacks for success and error paths.

This article assumes some familiarity with Promises; if you need a refresher, see the Introduction to JavaScript Promises. Fetch is supported in Chrome 42+, Firefox 39+, Safari 10.1, and Edge 14 (source).

Requesting a URL and Parsing JSON

A basic XHR request for a URL that returns JSON needs two event listeners plus calls to open() and send():

function reqListener () {
  const data = JSON.parse(this.responseText);
  console.log(data);
}

function reqError (err) {
  console.log('Fetch Error :-S', err);
}

const oReq = new XMLHttpRequest();
oReq.onload = reqListener;
oReq.onerror = reqError;
oReq.open('get', './api/some.json', true);
oReq.send();

The equivalent fetch() call collapses that into a single request. The response is a Stream object, so parsing happens asynchronously: calling json() returns another Promise that resolves with the parsed object.

fetch('./api/some.json')
  .then(response => {
    if (response.status !== 200) {
      console.log(`Looks like there was a problem. Status Code: ${response.status}`);

      return;
    }

    // Examine the text in the response
    response.json().then(function(data) {
      console.log(data);
    });
  })
  .catch(err => {
    console.log('Fetch Error :-S', err);
  });

Note that you check response.status yourself before parsing, because a fetch() request only rejects on network failure—an HTTP error status still resolves.

Inspecting Response Headers

The Response object also exposes headers. You can look up individual values or iterate the full set:

fetch('users.json').then(response => {
  console.log(response.headers.get('Content-Type'));
  console.log(response.headers.get('Date'));

  console.log(response.status);
  console.log(response.statusText);
  console.log(response.type);
  console.log(response.url);
});

Response Types and Request Modes

Every response carries a response.type that tells you where the resource came from:

  • basic — same-origin request. This is what most examples produce.
  • cors — cross-origin request where the server returned CORS headers. Header access is limited to Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, and Pragma.
  • opaque — cross-origin request with no CORS headers. You cannot read the response data, inspect the status, or determine whether the request succeeded.

You can constrain which responses resolve by setting a mode in the options object passed as the second argument to fetch(). Supported modes:

  • same-origin — succeeds only for same-origin assets; rejects everything else.
  • cors — allows same-origin and CORS-enabled cross-origin requests.
  • cors-with-forced-preflight — runs a preflight check before any request.
  • no-cors — intended for cross-origin requests that lack CORS headers, producing an opaque response; this is not currently possible in the window global scope.

Example of setting a mode explicitly:

fetch('http://some-site.com/cors-enabled/some.json', {mode: 'cors'})
  .then(response => response.text())
  .then(text => {
    console.log('Request successful', text);
  })
  .catch(error => {
    log('Request failed', error)
  });

Reusing Status and JSON Logic

Promises chain well, and fetch() lets you factor out the repetitive parts of a request. With a JSON API you usually check status, then parse. Those two steps can live in their own functions, leaving the request chain to handle only the data and any errors:

function status (response) {
  if (response.status >= 200 && response.status < 300) {
    return Promise.resolve(response)
  } else {
    return Promise.reject(new Error(response.statusText))
  }
}

function json (response) {
  return response.json()
}

fetch('users.json')
  .then(status)
  .then(json)
  .then(data => {
    console.log('Request succeeded with JSON response', data);
  }).catch(error => {
    console.log('Request failed', error);
  });

Here status returns Promise.resolve() when response.status is OK and Promise.reject() otherwise. If it resolves, the next .then() calls json(), which yields another Promise for the parsed body; a .catch() handles failures at either stage. Reusing status and json across requests keeps handlers small and testable.

POST Requests

For POST calls, pass method and body inside the options object:

fetch(url, {
    method: 'post',
    headers: {
      "Content-type": "application/x-www-form-urlencoded; charset=UTF-8"
    },
    body: 'foo=bar&lorem=ipsum'
  })
  .then(json)
  .then(data => {
    console.log('Request succeeded with JSON response', data);
  })
  .catch(error => {
    console.log('Request failed', error);
  });

Sending Credentials

To include cookies with a request, set credentials to "include":

fetch(url, {
  credentials: 'include'
})