The Fetch API and JSON

Using the Promise and Response objects of the Fetch API to access files with JSON-formatted data.

Learning Goals

At the end of this Tutorial, you will be able to:

  • Create text files in JSON-format.
  • Use the asynchronous Fetch API to retrieve files from a remote server.
  • Understand the roles of the Promise and Response objects.
  • Check for network errors with the try...catch (error) syntax.

For this Tutorial, in your javascript/exercises folder, create two new HTML files named list-products.html and list-users.html.

Create two new empty text files named list-products.js and list-users.js and save them in this same folder.

Add links to list-products.js and list-users.js in your list-products.html and list-users.html files using <script> tags with the defer attribute.

Make a sub-folder named data and in it, create two empty text files named products.json and users.json.

Introduction

Up until now, all data (variables, arrays, and objects) has been hard-coded inside the .js file. In the real world, data lives outside the code - in files or databases on the same or different servers.

To retrieve data from such sources, you need to learn about:

  • JSON-format: This is the typical format for working with external data.
  • Fetch API: Makes it possible to retrieve external data without the web page re-loading.
  • Live server: For security reasons, the Fetch API runs only on a webserver.

What is JSON?

In the Arrays of objects tutorial, you learnt that spreadsheet-style structured data tables in rows and columns can be represented in JavaScript by an array of objects.

screenshot
  • Each record row is an object (with curly braces {}).
  • Each object has the same keys (matching the spreadsheet columns).
  • The items (objects) are separated by commas ,.
  • The entire dataset is an array (with square brackets []).

Working with JSON-format data

JSON stands for JavaScript Object Notation. It is a common format for storing and moving data between files and web pages.

JSON data looks similar to JavaScript objects but has some key differences:

  • Keys (property names) must be in double quotes. For example, "price" and "age".
  • String values must be in double quotes. For example, "laptop" and "Smith".
  • No trailing commas , after the last value in objects.

Creating two sample JSON files

Below are two examples of files with JSON-format data.

Copy the data below to your products.json file in your /data subfolder.

[
  { "productID": 1001, "name": "Laptop", "price": 999.99, "inStock": true },
  { "productID": 1002, "name": "Wireless Mouse", "price": 24.99, "inStock": true },
  { "productID": 1003, "name": "Mechanical Keyboard", "price": 59.99, "inStock": false },
  { "productID": 1004, "name": "HD Monitor 24-inch", "price": 149.99, "inStock": true },
  { "productID": 1005, "name": "USB-C Hub", "price": 34.99, "inStock": true },
  { "productID": 1006, "name": "External Hard Drive 1TB", "price": 79.99, "inStock": true },
  { "productID": 1007, "name": "Noise Cancelling Headphones", "price": 199.99, "inStock": false },
  { "productID": 1008, "name": "Webcam 1080p", "price": 49.99, "inStock": true },
  { "productID": 1009, "name": "HDMI Cable 2m", "price": 12.50, "inStock": true },
  { "productID": 1010, "name": "Graphics Tablet", "price": 129.99, "inStock": false }
]

And copy this data to your users.json file in your /data subfolder.

[
  { "firstName": "Luis", "lastName": "Martinez", "age": 32, "isActive": true },
  { "firstName": "Emma", "lastName": "Smith", "age": 28, "isActive": false },
  { "firstName": "Tom", "lastName": "Murphy", "age": 21, "isActive": true },
  { "firstName": "Sophia", "lastName": "Brown", "age": 45, "isActive": true },
  { "firstName": "James", "lastName": "Wilson", "age": 35, "isActive": false },
  { "firstName": "Olivia", "lastName": "Miller", "age": 29, "isActive": true },
  { "firstName": "Daniel", "lastName": "Taylor", "age": 52, "isActive": true },
  { "firstName": "Ava", "lastName": "Anderson", "age": 24, "isActive": false },
  { "firstName": "Lucas", "lastName": "Thomas", "age": 39, "isActive": true },
  { "firstName": "Mia", "lastName": "Jackson", "age": 31, "isActive": true }
]

A very basic example

The Fetch API is a method of JavaScript's global Window object and enables you to send HTTP requests to web servers and handle the responses.

You need to supply only a single argument to the fetch() method: the URL of the data that you want to fetch. This URL is known as an endpoint. See below.

const response = await fetch("url");

The response variable holds a reference to the Response object returned by the Fetch API, which lets you read properties like response.ok and call methods like response.json() to parse the content.

Below is a very basic example if the Fetch API in action without error handling. Copy this to your list-products.js file:

async function fetchProductData() {
    // Create a response object to hold the remote data
    const response = await fetch("data/products.json");
    // Convert the response object to a parsed JavaScript object
    const data = await response.json();
    // Verify the code is working by logging the data to the console
    console.log(data);
}

// Call the function to execute the fetch request
fetchProductData();

Next, copy this to your list-users.js file:

async function fetchUserData() {
    // Create a response object to hold the remote data
    const response = await fetch("data/users.json");
    // Convert the response object to a parsed JavaScript object
    const data = await response.json();
    // Verify the code is working by logging the data to the console
    console.log(data);
}

// Call the function to execute the fetch request
fetchUserData();

CORS and the file:// protocol

Cross-origin resource sharing (CORS) is a browser mechanism which controls access to resources located outside of a given domain. For security reasons, CORS blocks JavaScript Fetch API requests when running directly from the file system. Web pages opened as file:// have an opaque origin (often treated as null).

screenshot

For your list-products.html and list-users.html pages and their scripts to work correctly, you must run them through a local web server, such as the VS Code Live Server extension.

Ensure that you have the Live Server extension installed and running, and that your two HTML files are in your currently-selected folder.

screenshot screenshot

Finally, open the Console for your two HTML files to see the fetched data.

screenshot

The Fetch API is asynchronous

The Fetch API provides an async/await syntax for writing asynchronous code.

async function getSomeData() {
    const response = await fetch("path/to/file.json");
    // More code to process the response
}

This means:

  • The await keyword inside the getSomeData() function pauses the code until a response arrives from the server, which may take a number of seconds. It says: "Stop executing on this line. Don't move to the next line until a response comes back."
  • Any delay by the fetch API in retrieving the remote data does not block the main thread of your code, From a user's perspective, the web page remains responsive and does not 'freeze.'

Working with the Promise and Response objects

The Fetch API uses two built-in JavaScript objects: the Promise and the Response objects. The async keyword before the function declaration makes the function first return a Promise.

This Promise object can have one of three possible statuses:

pending

The request's initial state.

fulfilled

The request has completed successfully and a value is returned.

rejected

The request has failed and a reason (error) is returned.

A Promise is said to be settled or resolved if it is either fulfilled or rejected, but not pending.

When the request completes successfully, the Promise resolves with a Response object.

You can think of the Response object as a 'package' that contains the headers, status code, security details and data returned from the server. It also contains built-in methods (see below) for working with the data.

The .json() and .text() methods

The data from the server is not directly accessible from the Response object. A Response is metadata plus a body stream. So you need to parse the data using one of two methods:

.text()

Use this when the Response object contains 'raw' or 'plain' text, or HTML-formatted text.

.json()

Use this when the Response object contains only data in JSON format.

Adding error-checking

Because a Fetch API request may be unsuccessful, it is important to add error-checking to your code. Update your list-products.js and list-users.js scripts with if/else branches as shown below. Note the 'bad' branch comes first.

async function fetchProductData() {
    // Create a response object to hold the remote data
    const response = await fetch("data/products.json");
    if (!response.ok) {
       // If response has an error status code, log a message
       // This handles cases like 404 (not found), 500 (server error), etc.
       console.log(`Network response was not ok - Status: ${response.status} ${response.statusText}`);
       return; // Stop executing the function if there was an error
    }
    else {
      // Convert the response object to a parsed JavaScript object
      const data = await response.json();
      // Verify the code is working by logging the data to the console
      console.log(data);
  }
}

 

async function fetchUserData() {
    // Create a response object to hold the remote data
    const response = await fetch("data/users.json");
    if (!response.ok) {
       // If response has an error status code, log a message
       // This handles cases like 404 (not found), 500 (server error), etc.
       console.log(`Network response was not ok - Status: ${response.status} ${response.statusText}`);
       return; // Stop executing the function if there was an error
    }
    else {
      // Convert the response object to a parsed JavaScript object
      const data = await response.json();
      // Verify the code is working by logging the data to the console
      console.log(data);
  }
}

This will show both the numeric status code (like 404, 500, etc.) and the associated status text (like "Not Found" or "Internal Server Error"). So you will have detailed information about what went wrong with the request.

100-199

Informational responses

200-299

Success responses

300-399

Redirection messages

400-499

Client error responses

500-599

Server error responses

Checking for errors with try...catch (error)

The if(!response.ok) condition handles situations where the server responded properly - even if the response was an error code such as 404 (Not Found) or 500 (Internal Server Error).

Wrapping all the code inside your function with a try-catch (error) clause provides wider protection against other types of errors.

About the try-catch (error) syntax

The try-catch statement allows you to test a block of code for errors and handle any exceptions that occur. The basic syntax is as follows.

try {
    // Code that might cause an error
} catch (error) {
    // Code to handle the error
}

Here is how it works:

  • The try block contains the code that might throw an error.
  • If an error occurs, execution of that block stops immediately.
  • Control transfers to the catch block.
  • The catch parameter (commonly named error or err) contains information about the error.
  • After the catch (error) block executes, the program continues running after the try...catch statement.

In summary:

if (!response.ok) {}

This handles HTTP errors. The Fetch() API considers a "404 Not Found" or a "500 Server Error" to be a successful network connection. The server has successfully answered, "I don't have this file!"

try...catch (error)

This handles network and code failures. It triggers if the user loses their internet connection, if the DNS fails, or if the response.json() method crashes because the file is corrupted.

Let's do a final update on your two Fetch API scripts. Here is list-products.js.

async function fetchProductData() {
   try {
       // The 'await' keyword pauses execution until the fetch Promise resolves
       const response = await fetch("data/products.json");
       if (!response.ok) {
          // If response has an error status code, log a message
          // This handles cases like 404 (not found), 500 (server error), etc.
          console.log(`Network response was not ok - Status: ${response.status} ${response.statusText}`);
          return; // Stop executing the function if there was an error
        }
        else {
          // For successful responses, convert the response object to a parsed JavaScript object
          const data = await response.json();
          // Output the parsed user data object to the console
          console.log(data);
        }
    } // End try block
    catch (error) {
        // Catch network errors, JSON parsing errors, or any other exceptions
        console.error(`Error fetching product data: ${error}`);
    }
}

fetchProductData();

And here is the final version of list-users.js.

async function fetchUserData() {
   try {
       // The 'await' keyword pauses execution until the fetch Promise resolves
       const response = await fetch("data/users.json");
       if (!response.ok) {
          // If response has an error status code, log a message
          // This handles cases like 404 (not found), 500 (server error), etc.
          console.log(`Network response was not ok - Status: ${response.status} ${response.statusText}`);
          return; // Stop executing the function if there was an error
        }
        else {
          // For successful responses, convert the response object to a parsed JavaScript object
          const data = await response.json();
          // Output the parsed user data object to the console
          console.log(data);
        }
    } // End try block
    catch (error) {
        // Catch network errors, JSON parsing errors, or any other exceptions
        console.error(`Error fetching user data: ${error}`);
    }
}

fetchUserData();

Outputting data to the web page

Now that you have successfully fetched and parsed the JSON data, let's display it on the web page. We will use the .forEach() method to loop through the array and dynamically generate HTML for each item.

Let's begin by adding an empty <div> element with an ID of products-container to the list-products.html file. This is where JavaScript will insert the generated content.

<div id="products-container"></div>

Now, add the following function call at the end of the 'success path' try block in your fetchProductData() function in your list-products.js script file:

displayProducts(data);

Next, add this new function at the bottom of your list-products.js file:

// Function to handle the DOM output
function displayProducts(productsArray) {
    const container = document.getElementById("products-container");
    let htmlOutput = "";

    // Loop through each product in the array
    productsArray.forEach(product => {
        // Build the HTML string using compound assignment operator and template literals
        htmlOutput += `
            <p><b>${product.name}</b>  Price: $${product.price}  In Available?: ${product.inStock}</p>
        `;
    });

    // Output the final HTML to the page
    container.innerHTML = htmlOutput;
}

Repeat the above steps for your list-users.html and list-users.js files.

Try it yourself

In your script file...

---

Change the fetch URL in one of your JavaScript sample files to the name of a file that does not exist - for example, data/wrong-file.json. Open your browser console and observe how your if (!response.ok) { } block catches the 404 error and logs the status.

More learning resources

Tutorial Quiz

  Take the test

Tutorial Podcast

Sample AI prompts

Explain the JavaScript fetch API and the concepts of async and await using a real-world analogy, like ordering food at a restaurant.
What is a CORS error in JavaScript? Explain it simply and tell me the most common ways developers fix or bypass it during local development.