Using Fetch

Whenever we send or retrieve information with JavaScript, we initiate a thing known as an Ajax call. Ajax is a technique to send and retrieve information behind the scenes without needing to refresh the page. It allows browsers to send and retrieve information, then do things with what it gets back, like add or change HTML on the page.

Let's take a look at the history of that and then bring ourselves up-to-date.

Another note here, we're going to be using ES6 syntax for all the demos in this article.

A few years ago, the easiest way to initiate an Ajax call was through the use of jQuery's ajax method:

$.ajax('some-url', {
  success: (data) => { /* do something with the data */ },
  error: (err) => { /* do something when an error happens */}
});

We could do Ajax without jQuery, but we had to write an XMLHttpRequest, which is pretty complicated.

Thankfully, browsers nowadays have improved so much that they support the Fetch API, which is a modern way to Ajax without helper libraries like jQuery or Axios. In this article, I'll show you how to use Fetch to handle both success and errors.

Support for Fetch

Let's get support out of the way first.

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

ChromeOperaFirefoxIEEdgeSafari
422939No1410.1

Mobile / Tablet

iOS SafariOpera MobileOpera MiniAndroidAndroid ChromeAndroid Firefox
10.337No566156

Support for Fetch is pretty good! All major browsers (with the exception of Opera Mini and old IE) support it natively, which means you can safely use it in your projects. If you need support anywhere it isn't natively supported, you can always depend on this handy polyfill.

Getting data with Fetch

Getting data with Fetch is easy. You just need to provide Fetch with the resource you're trying to fetch (so meta!).

Let's say we're trying to get a list of Chris' repositories on Github. According to Github's API, we need to make a get request for api.github.com/users/chriscoyier/repos.

This would be the fetch request:

fetch('https://api.github.com/users/chriscoyier/repos');

So simple! What's next?

Fetch returns a Promise, which is a way to handle asynchronous operations without the need for a callback.

To do something after the resource is fetched, you write it in a .then call:

fetch('https://api.github.com/users/chriscoyier/repos')
  .then(response => {/* do something */})

If this is your first encounter with Fetch, you'll likely be surprised by the response Fetch returns. If you console.log the response, you'll get the following information:

{
  body: ReadableStream
  bodyUsed: false
  headers: Headers
  ok : true
  redirected : false
  status : 200
  statusText : "OK"
  type : "cors"
  url : "http://some-website.com/some-url"
  __proto__ : Response
}

Here, you can see that Fetch returns a response that tells you the status of the request. We can see that the request is successful (ok is true and status is 200), but a list of Chris' repos isn't present anywhere!

Turns out, what we requested from Github is hidden in body as a readable stream. We need to call an appropriate method to convert this readable stream into data we can consume.

Since we're working with GitHub, we know the response is JSON. We can call response.json to convert the data.

There are other methods to deal with different types of response. If you're requesting an XML file, then you should call response.text. If you're requesting an image, you call response.blob.

All these conversion methods (response.json et all) returns another Promise, so we can get the data we wanted with yet another .then call.

fetch('https://api.github.com/users/chriscoyier/repos')
  .then(response => response.json())
  .then(data => {
    // Here's a list of repos!
    console.log(data)
  });

Phew! That's all you need to do to get data with Fetch! Short and simple, isn't it? :)

Next, let's take a look at sending some data with Fetch.

Sending data with Fetch

Sending data with Fetch is pretty simple as well. You just need to configure your fetch request with three options.

fetch('some-url', options);

The first option you need to set is your request method to post, put or del. Fetch automatically sets the method to get if you leave it out, which is why getting a resource takes lesser steps.

The second option is to set your headers. Since we're primarily sending JSON data in this day and age, we need to set Content-Type to be application/json.

The third option is to set a body that contains JSON content. Since JSON content is required, you often need to call JSON.stringify when you set the body.

In practice, a post request with these three options looks like:

let content = {some: 'content'};

// The actual fetch request
fetch('some-url', {
  method: 'post',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(content)
})
// .then()...

For the sharp-eyed, you'll notice there's some boilerplate code for every post, put or del request. Ideally, we can reuse our headers and call JSON.stringify on the content before sending since we already know we're sending JSON data.

But even with the boilerplate code, Fetch is still pretty nice for sending any request.

Handling errors with Fetch, however, isn't as straightforward as handling success messages. You'll see why in a moment.

Handling errors with Fetch

Although we always hope for Ajax requests to be successful, they can fail. There are many reasons why requests may fail, including but not limited to the following:

  1. You tried to fetch a non-existent resource.
  2. You're unauthorized to fetch the resource.
  3. You entered some arguments wrongly
  4. The server throws an error.
  5. The server timed out.
  6. The server crashed.
  7. The API changed.
  8. ...

Things aren't going to be pretty if your request fails. Just imagine a scenario you tried to buy something online. An error occured, but it remains unhandled by the people who coded the website. As a result, after clicking buy, nothing moves. The page just hangs there... You have no idea if anything happened. Did your card go through? 😱.

Now, let's try to fetch a non-existent error and learn how to handle errors with Fetch. For this example, let's say we misspelled chriscoyier as chrissycoyier

// Fetching chrissycoyier's repos instead of chriscoyier's repos
fetch('https://api.github.com/users/chrissycoyier/repos')

We already know we should get an error since there's no chrissycoyier on Github. To handle errors in promises, we use a catch call.

Given what we know now, you'll probably come up with this code:

fetch('https://api.github.com/users/chrissycoyier/repos')
  .then(response => response.json())
  .then(data => console.log('data is', data))
  .catch(error => console.log('error is', error));

Fire your fetch request. This is what you'll get:

Fetch failed, but the code that gets executed is the second `.then` instead of `.catch`

Why did our second .then call execute? Aren't promises supposed to handle errors with .catch? Horrible! 😱😱😱

If you console.log the response now, you'll see slightly different values:

{
  body: ReadableStream
  bodyUsed: true
  headers: Headers
  ok: false // Response is not ok
  redirected: false
  status: 404 // HTTP status is 404.
  statusText: "Not Found" // Request not found
  type: "cors"
  url: "https://api.github.com/users/chrissycoyier/repos"
}

Most of the response remain the same, except ok, status and statusText. As expected, we didn't find chrissycoyier on Github.

This response tells us Fetch doesn't care whether your AJAX request succeeded. It only cares about sending a request and receiving a response from the server, which means we need to throw an error if the request failed.

Hence, the initial then call needs to be rewritten such that it only calls response.json if the request succeeded. The easiest way to do so to check if the response is ok.

fetch('some-url')
  .then(response => {
    if (response.ok) {
      return response.json()
    } else {
      // Find some way to get to execute .catch()
    }
  });

Once we know the request is unsuccessful, we can either throw an Error or reject a Promise to activate the catch call.

// throwing an Error
else {
  throw new Error('something went wrong!')
}

// rejecting a Promise
else {
  return Promise.reject('something went wrong!')
}

Choose either one, because they both activate the .catch call.

Here, I choose to use Promise.reject because it's easier to implement. Errors are cool too, but they're harder to implement, and the only benefit of an Error is a stack trace, which would be non-existent in a Fetch request anyway.

So, the code looks like this so far:

fetch('https://api.github.com/users/chrissycoyier/repos')
  .then(response => {
    if (response.ok) {
      return response.json()
    } else {
      return Promise.reject('something went wrong!')
    }
  })
  .then(data => console.log('data is', data))
  .catch(error => console.log('error is', error));
Failed request, but error gets passed into catch correctly

This is great. We're getting somewhere since we now have a way to handle errors.

But rejecting the promise (or throwing an Error) with a generic message isn't good enough. We won't be able to know what went wrong. I'm pretty sure you don't want to be on the receiving end for an error like this...

Yeah... I get it that something went wrong... but what exactly? 🙁

What went wrong? Did the server time out? Was my connection cut? There's no way for me to know! What we need is a way to tell what's wrong with the request so we can handle it appropriately.

Let's take a look at the response again and see what we can do:

{
  body: ReadableStream
  bodyUsed: true
  headers: Headers
  ok: false // Response is not ok
  redirected: false
  status: 404 // HTTP status is 404.
  statusText: "Not Found" // Request not found
  type: "cors"
  url: "https://api.github.com/users/chrissycoyier/repos"
}

Okay great. In this case, we know the resource is non-existent. We can return a 404 status or Not Found status text and we'll know what to do with it.

To get status and statusText into the .catch call, we can reject a JavaScript object:

fetch('some-url')
  .then(response => {
    if (response.ok) {
      return response.json()
    } else {
      return Promise.reject({
        status: response.status,
        statusText: response.statusText
      })
    }
  })
  .catch(error => {
    if (error.status === 404) {
      // do something about 404
    }
  })

Now we're getting somewhere again! Yay! 😄.

Let's make this better! 😏.

The above error handling method is good enough for certain HTTP statuses which doesn't require further explanation, like:

  • 401: Unauthorized
  • 404: Not found
  • 408: Connection timeout
  • ...

But it's not good enough for this particular badass:

  • 400: Bad request.

What constitutes bad request? It can be a whole slew of things! For example, Stripe returns 400 if the request is missing a required parameter.

Stripe's explains it returns a 400 error if the request is missing a required field

It's not enough to just tell our .catch statement there's a bad request. We need more information to tell what's missing. Did your user forget their first name? Email? Or maybe their credit card information? We won't know!

Ideally, in such cases, your server would return an object, telling you what happened together with the failed request. If you use Node and Express, such a response can look like this.

res.status(400).send({
  err: 'no first name'
})

Here, we can't reject a Promise in the initial .then call because the error object from the server can only be read after response.json.

The solution is to return a promise that contains two then calls. This way, we can first read what's in response.json, then decide what to do with it.

Here's what the code looks like:

fetch('some-error')
  .then(handleResponse)

function handleResponse(response) {
  return response.json()
    .then(json => {
      if (response.ok) {
        return json
      } else {
        return Promise.reject(json)
      }
    })
}

Let's break the code down. First, we call response.json to read the json data the server sent. Since, response.json returns a Promise, we can immediately call .then to read what's in it.

We want to call this second .then within the first .then because we still need to access response.ok to determine if the response was successful.

If you want to send the status and statusText along with the json into .catch, you can combine them into one object with Object.assign().

let error = Object.assign({}, json, {
  status: response.status,
  statusText: response.statusText
})
return Promise.reject(error)

With this new handleResponse function, you get to write your code this way, and your data gets passed into .then and .catch automatically

fetch('some-url')
  .then(handleResponse)
  .then(data => console.log(data))
  .catch(error => console.log(error))

Unfortunately, we're not done with handling the response just yet :(

Handling other response types

So far, we've only touched on handling JSON responses with Fetch. This already solves 90% of use cases since APIs return JSON nowadays.

What about the other 10%?

Let's say you received an XML response with the above code. Immediately, you'll get an error in your catch statement that says:

Parsing an invalid JSON produces a Syntax error

This is because XML isn't JSON. We simply can't return response.json. Instead, we need to return response.text. To do so, we need to check for the content type by accessing the response headers:

.then(response => {
  let contentType = response.headers.get('content-type')

  if (contentType.includes('application/json')) {
    return response.json()
    // ...
  }

  else if (contentType.includes('text/html')) {
    return response.text()
    // ...
  }

  else {
    // Handle other responses accordingly...
  }
});

Wondering why you'll ever get an XML response?

Well, I encountered it when I tried using ExpressJWT to handle authentication on my server. At that time, I didn't know you can send JSON as a response, so I left it as its default, XML. This is just one of the many unexpected possibilities you'll encounter. Want another? Try fetching some-url :)

Anyway, here's the entire code we've covered so far:

fetch('some-url')
  .then(handleResponse)
  .then(data => console.log(data))
  .catch(error => console.log(error))

function handleResponse (response) {
  let contentType = response.headers.get('content-type')
  if (contentType.includes('application/json')) {
    return handleJSONResponse(response)
  } else if (contentType.includes('text/html')) {
    return handleTextResponse(response)
  } else {
    // Other response types as necessary. I haven't found a need for them yet though.
    throw new Error(`Sorry, content-type ${contentType} not supported`)
  }
}

function handleJSONResponse (response) {
  return response.json()
    .then(json => {
      if (response.ok) {
        return json
      } else {
        return Promise.reject(Object.assign({}, json, {
          status: response.status,
          statusText: response.statusText
        }))
      }
    })
}
function handleTextResponse (response) {
  return response.text()
    .then(text => {
      if (response.ok) {
        return json
      } else {
        return Promise.reject({
          status: response.status,
          statusText: response.statusText,
          err: text
        })
      }
    })
}

It's a lot of code to write/copy and paste into if you use Fetch. Since I use Fetch heavily in my projects, I create a library around Fetch that does exactly what I described in this article (plus a little more).

Introducing zlFetch

zlFetch is a library that abstracts away the handleResponse function so you can skip ahead to and handle both your data and errors without worrying about the response.

A typical zlFetch look like this:

zlFetch('some-url', options)
  .then(data => console.log(data))
  .catch(error => console.log(error));

To use zlFetch, you first have to install it.

npm install zl-fetch --save

Then, you'll import it into your code. (Take note of default if you aren't importing with ES6 imports). If you need a polyfill, make sure you import it before adding zlFetch.

// Polyfills (if needed)
require('isomorphic-fetch') // or whatwg-fetch or node-fetch if you prefer

// ES6 Imports
import zlFetch from 'zl-fetch';

// CommonJS Imports
const zlFetch = require('zl-fetch');

zlFetch does a bit more than removing the need to handle a Fetch response. It also helps you send JSON data without needing to write headers or converting your body to JSON.

The below the functions do the same thing. zlFetch adds a Content-Type and converts your content into JSON under the hood.

let content = {some: 'content'}

// Post request with fetch
fetch('some-url', {
  method: 'post',
  headers: {'Content-Type': 'application/json'}
  body: JSON.stringify(content)
});

// Post request with zlFetch
zlFetch('some-url', {
  method: 'post',
  body: content
});

zlFetch also makes authentication with JSON Web Tokens easy.

The standard practice for authentication is to add an Authorization key in the headers. The contents of this Authorization key is set to Bearer your-token-here. zlFetch helps to create this field if you add a token option.

So, the following two pieces of code are equivalent.

let token = 'someToken'
zlFetch('some-url', {
  headers: {
    Authorization: `Bearer ${token}`
  }
});

// Authentication with JSON Web Tokens with zlFetch
zlFetch('some-url', {token});

That's all zlFetch does. It's just a convenient wrapper function that helps you write less code whenever you use Fetch. Do check out zlFetch if you find it interesting. Otherwise, feel free to roll your own!

Here's a Pen for playing around with zlFetch:

See the Pen zlFetch demo by Zell Liew (@zellwk) on CodePen.

Wrapping up

Fetch is a piece of amazing technology that makes sending and receiving data a cinch. We no longer need to write XHR requests manually or depend on larger libraries like jQuery.

Although Fetch is awesome, error handling with Fetch isn't straightforward. Before you can handle errors properly, you need quite a bit of boilerplate code to pass information go to your .catch call.

With zlFetch (and the info presented in this article), there's no reason why we can't handle errors properly anymore. Go out there and put some fun into your error messages too :)


By the way, if you liked this post, you may also like other front-end-related articles I write on my blog. Feel free to pop by and ask any questions you have. I'll get back to you as soon as I can.