Self-paced

Explore our extensive collection of courses designed to help you master various subjects and skills. Whether you're a beginner or an advanced learner, there's something here for everyone.

Bootcamp

Learn live

Join us for our free workshops, webinars, and other events to learn more about our programs and get started on your journey to becoming a developer.

Upcoming live events

Learning library

For all the self-taught geeks out there, here is our content library with most of the learning materials we have produced throughout the years.

It makes sense to start learning by reading and watching videos about fundamentals and how things work.

Full-Stack Software Developer - 16w

Data Science and Machine Learning - 16 wks

Search from all Lessons


LoginGet Started
โ† Back to Lessons

Weekly Coding Challenge

Every week, we pick a real-life project to build your portfolio and get ready for a job. All projects are built with ChatGPT as co-pilot!

Start the Challenge

Podcast: Code Sets You Free

A tech-culture podcast where you learn to fight the enemies that blocks your way to become a successful professional in tech.

Listen the podcast
  • ajax

  • HTTP

  • Fetch API

Edit on Github

The Fetch API

Now Let's Use fetch() with Async/Await

A Quick Glance at the Fetch API

Think of a fetch as a simple JavaScript Promise. You send a request to the server/API and expect to receive a response. The Fetch API gives us the fetch() method, which allows us to access those requests and responses using JavaScript.

Let's look at what a simple fetch looks like:

1fetch('examples/example.json') 2 .then(response => { 3 // Here is where you put what you want to do with the response 4 }) 5 .catch(error => { 6 console.log('Oh No! There was a problem: \n', error); 7 });

What is happening here?

  1. We pass the URI (path) we want to fetch from ('examples/examples.json') as an argument of the fetch method.
  2. The call returns a "promise" which eventually resolves into a response. Note that a "promise" is not the actual response. Think of it as a proxy for the response.
  3. Once the response comes through, it is passed to the .then method for use.
  4. If there is an error in completing the request, for example, if there is no network connection, the .catch is passed with the appropriate error as a parameter. Note that an error like 404 (a bad response) is still a valid response (the server returned something to us) and therefore wouldn't be considered an uncompleted response. So, in the example above, it would not automatically fall back to the .catch.

How do we check for a successful response?

Since the fetch promise would only reject a request if it was unable to complete it, we need to manually validate if a response is good or throw an error if it's not.

A generic response from a server looks something like this when logged on the console:

1[object Response] { 2 body: (...) 3 bodyUsed: false 4 headers: Headers {} 5 ok: true 6 redirected: false 7 status: 200 8 statusText: "OK" 9 type: "cors" 10 url: "https://assets.breatheco.de/apis/fake/todos/user/Gmihov" 11}

With that in mind, to evaluate the status of a response, you can use its properties:

  • response.ok - checks for a status in the 200s and returns a boolean.
  • response.status - returns an integer with the response status code. Default is 200.
  • response.statusText - returns a string whose default is "OK" or a relevant error message.

Now we can update the example from above to validate the response

1fetch('examples/example.json') 2 .then(response => { 3 if (!response.ok) { 4 throw new Error(response.statusText); 5 } 6 // Here is where you put what you want to do with the response 7 }) 8 .catch(error => { 9 console.log('Looks like there was a problem: \n', error); 10 });

Now what's happening?

  1. We are still passing the URI ('examples/example.json') as an argument.
  2. The fetch returns a promise that eventually becomes the response.
  3. The response is then passed to .then to be used as you specify.
  4. We have an if-statement that basically says if response.ok is not true, throw an error with the response.statusText, which will then trigger the .catch.

Why do we need this?

To prevent bad responses from going down the chain and breaking your code later on.

We need to throw this error manually because, as explained above, error messages received within a response from the server do not register automatically as an error and do not show up in the .catch method.

The result will be that the fetch will deliver nothing, and yet the client will be clueless that something has gone wrong.

Now What?

Now we need to "read" the response in order to access the body of the response.

As you already know, the only data that can travel over an HTTP connection is in plain text format. Therefore, we need to convert the plain text from the body of the response into meaningful JavaScript format.

Luckily, there's a method for that: .json(); which we apply to the response as response.json().

1fetch('examples/example.json') 2 .then(response => { 3 if (!response.ok) { 4 throw Error(response.statusText); 5 } 6 // Read the response as JSON 7 return response.json(); 8 }) 9 .then(responseAsJson => { 10 // Do stuff with the JSONified response 11 console.log(responseAsJson); 12 }) 13 .catch(error => { 14 console.log('Looks like there was a problem: \n', error); 15 });

Now what is going on?

Simple. Think of it in separate steps.

  1. Fetch the resource at the given path. (Fetch gets the path to the resource and returns a promise that will resolve to a response object).

  2. Then validate the response. (This checks to see if the response is valid (200s). If not, skip to step 5).

  3. Read the response as JSON.

  4. Log the result. (The result is the JSON data received from the body of the response).

  5. Catch any errors.

Now that you have seen the basics, we can compose more advanced fetches.

The default request method is the "GET" method; which is what we have seen so far.

The most used methods and what they represent are:

  • GET: Read/Retrieve.
  • POST: Create.
  • PUT: Edit/Update.
  • DELETE: You guessed it, this simply means delete.

Here's an example of a POST method that creates a new user:

1fetch('https://example.com/users.json', { 2 method: 'POST', 3 mode: 'cors', 4 redirect: 'follow', 5 headers: new Headers({ 6 'Content-Type': 'text/plain' 7 }) 8}) 9 .then(res => res.json()) 10 .then(response => { /* handle response */ }) 11 .catch(error => console.error(error));

Note: that this example fetch is posting (sending to the server) data in plain text format. In modern front end development, this is less common. The most common content type for our methods will be the application/json format, as seen in the following example:

1fetch('https://example.com/users', { 2 method: 'PUT', // or 'POST' 3 body: JSON.stringify(data), // data can be a 'string' or an {object} which comes from somewhere further above in our application 4 headers: { 5 'Content-Type': 'application/json' 6 } 7}) 8 .then(res => { 9 if (!res.ok) throw Error(res.statusText); 10 return res.json(); 11 }) 12 .then(response => console.log('Success:', response)) 13 .catch(error => console.error(error));

โ˜ Did you notice something new above?

The "body" of the fetch is where we place the data that we want to send to the server for permanent storage with our POST or PUT requests. Because we can only send plain text over HTTP, we have to convert our data from its original JS format to a string. We do that with the JSON.stringify() method.

Requests with the GET or DELETE methods do not require a body since normally they are not expected to send any data, however, you can include a body in those requests as well, if needed.

HTTP headers allow us to perform additional actions on the request and response. You can set request headers as you see above.

One of the most common headers needed is the 'Content-Type' header. It signals to the recipient of the request (the server) how it should treat the data contained in the body of the request. Because most of the time we send data in some JavaScript format which is then stringified, we need to instruct the server to convert the string that it receives back into a JSON format, as seen in line 5 above.

Headers can be sent in a request and received in a response.

Therefore, you can use the headers of the response you receive from the server to check the content type of its body and make sure you are receiving the right format before going any further in the process. An example of this would be:

1fetch('https://example.com/users') 2 .then(response => { 3 let contentType = response.headers.get("content-type"); 4 if (contentType && contentType.includes("application/json")) { 5 return response.json(); 6 } 7 throw new TypeError("Sorry, There's no JSON here!"); 8 }) 9 .then(jsonifiedResponse => { /* do whatever you want with the jsonified response */ }) 10 .catch(error => console.log(error));

Note that a Header method will throw a TypeError if the name used is not a valid HTTP header name. A list of valid headers can be found here.

Now Let's Use fetch() with Async/Await

JavaScript provides us an alternative way to make HTTP requests using fetch() with Async/Await

  • async makes a function return a Promise
  • await makes a function wait for a Promise

GET Method

Let's start with the GET method and analyze it

1const getData = async () => { 2 const response = await fetch('https://example.com/users'); 3 if (response.ok) { 4 const data = await response.json(); 5 return data; 6 } else { 7 console.log('error: ', response.status, response.statusText); 8 /* Handle the error returned by the HTTP request */ 9 return {error: {status: response.status, statusText: response.statusText}}; 10 }; 11};

๐Ÿ“ Remember that GET is the default method. Therefore, it is not mandatory to write the second parameter of fetch()

Let's Analyze This Function

  1. We define an arrow function and name it with a significant name, in this example, getData.
  2. We determine that this arrow function will be asynchronous async because it is an HTTP request and its response is not immediate.
  3. Next, we define a constant response that will await the response from fetch(). In this constant, we store the response of the request.
  4. We then evaluate the response. (This checks if the response is valid (200). If not, go to step 6.
  5. If the response is valid, in the constant data, we will store the data in JSON format and return this response.
  6. If the response is not valid, we log the error provided by the HTTP protocol (100, 300, 400, 500) and, if necessary, take actions to handle it.

๐Ÿ“ Note: The HTTP protocol will always provide a response. Whether that response is good or not will be known through the HTTP Response Status Codes.

POST Method with async

Now that we know how it works, let's see an example of the GET method

1const createData = async () => { 2 const response = await fetch('https://example.com/users', { 3 method: 'POST', 4 body: JSON.stringify(dataToSend), // the variable dataToSend can be a 'string' or an {object} that comes from somewhere else in our application 5 headers: { 6 'Content-Type': 'application/json' 7 } 8 }); 9 if (response.ok) { 10 const data = await response.json(); 11 return data; 12 } else { 13 console.log('error: ', response.status, response.statusText); 14 /* Handle the error returned by the HTTP request */ 15 return {error: {status: response.status, statusText: response.statusText}}; 16 }; 17};

Here we add the second parameter of fetch() to include the method, body, and headers.

PUT Method with async

Example of a request with the PUT method

1const updateData = async () => { 2 const response = await fetch('https://example.com/users', { 3 method: 'PUT', 4 body: JSON.stringify(dataToSend), // the variable dataToSend can be a 'string' or an {object} that comes from somewhere else in our application 5 headers: { 6 'Content-Type': 'application/json' 7 } 8 }); 9 if (response.ok) { 10 const data = await response.json(); 11 return data; 12 } else { 13 console.log('error: ', response.status, response.statusText); 14 /* Handle the error returned by the HTTP request */ 15 return {error: {status: response.status, statusText: response.statusText}}; 16 }; 17}; 18 19

DELETE Method

Example of a request with the DELETE method.

1const deleteData = async () => { 2 const response = await fetch('https://example.com/users', { 3 method: 'DELETE', 4 }); 5 if (response.ok) { 6 const data = await response.json(); 7 return data; 8 } else { 9 console.log('error: ', response.status, response.statusText); 10 /* Handle the error returned by the HTTP request */ 11 return {error: {status: response.status, statusText: response.statusText}}; 12 }; 13};