JavaScript Fetch API: A Complete Overview

Graham BoyleGraham Boyle
13 min read

As web applications become more dynamic, efficient server communication is crucial. JavaScript's fetch API provides a powerful and flexible way to handle HTTP requests, allowing developers to interact with APIs and other remote resources. Mastering the fetch API is essential for modern web development, whether you are retrieving, sending, updating, or deleting data.

In this article, you will discover how to use the fetch API, from the basics to advanced techniques, by building an awesome webpage called Get Country Info! This will help you write efficient and maintainable code. Plus, we'll include a link to external APIs at the end of this article.

Prerequisites

What is Fetch API?

The fetch API is a modern JavaScript interface that lets developers make HTTP requests, like getting or sending data, to and from web servers. It offers a cleaner and more powerful way to handle asynchronous requests compared to older methods like XMLHttpRequest. The fetch API was introduced in ECMAScript 6 (ES6) in 2015.

The fetch() function takes two arguments: the resource URL (required) and an optional options object for configuring settings like the HTTP method, headers, and body. It returns a Promise that resolves to a Response object, regardless of whether the HTTP status code indicates success or failure. It's important to manually check the response.ok property to determine if the request was successful.

Now you might wonder, what exactly is a Promise? A Promise in JavaScript is an object that represents the eventual outcome (success or failure) of an asynchronous operation. It allows you to handle tasks like data fetching or file reading without stopping the rest of the code from running.

Promises help you manage asynchronous operations cleanly, avoiding "callback hell" (nested callbacks) and making your code easier to read and maintain. You can chain actions with .then() for success and .catch() for errors. Simply put, a promise is a placeholder for future values.

Note: Asynchronous operations are tasks that run independently of the main program flow, without blocking the execution of other parts of the code. This means they can run in the background while the rest of the program executes.

Key Features of Fetch API:

  • Promise-based: Fetch returns a Promise, making it easier to handle success and failure using .then() and .catch().

  • Supports all HTTP methods: You can use GET, POST, PUT, DELETE, etc., for various requests.

  • Readable and maintainable: The syntax is simpler and more intuitive than previous methods.

  • Built-in support for handling responses: You can easily work with different data formats like JSON, text, Blob, etc.

Example of a GET request:

fetch('https://api.example.com/data', {options}) //GET request, returns a promise
  .then(response => response.json()) //Convert the response to JSON
  .then(data => console.log(data)) //Log data to the console
  .catch(error => console.error('Error:', error)); //Catch and log errors

The code snippet above illustrates how to send a GET request to an external web API, handle the response, parse JSON data, and log the results or errors to the console. This helps developers build efficient, interactive web applications by managing server communication seamlessly.

Example of a POST request:

fetch('https://api.example.com/submit', {
  method: 'POST', // Specify the HTTP method as POST
  headers: {
    'Content-Type': 'application/json', // Set the request headers to indicate JSON format
  },
  body: JSON.stringify({ username: 'johndoe', password: '123456' }) // Send data as JSON
})
  .then(response => response.json()) // Convert the response to JSON
  .then(data => console.log(data)) // Log the response data
  .catch(error => console.error('Error:', error)); // Log any errors

Note: By default, the fetch() function uses the GET method to retrieve data from the server unless a different method (e.g., POST, PUT, DELETE) is explicitly defined in the options object.

Creating a basic HTML/CSS Page

Now that we understand the fetch API, let's dive in and put it to use! We'll start by building a simple HTML page with a search input and button to fetch data from an external API based on the user's search. This task will involve using the fetch API to retrieve country data, chaining methods for efficient processing, handling errors, and effectively displaying the fetched country details. Let’s get started!

<body>
    <section class="container">
      <h1>Country Info Finder</h1>

      <!-- Search Input -->
      <div class="search-box">
        <input
          type="text"
          id="country-input"
          placeholder="Enter a country name"
        />
        <button id="search-btn">Search</button>
      </div>

      <!-- Country Information Section -->
      <div class="country-info">
        <div id="error-message"></div>
        <div id="result">
          <h2 id="country-name"></h2>
          <img id="country-flag" alt="Country Flag" />
          <p><strong>Capital:</strong> <span id="capital"></span></p>
          <p><strong>Population:</strong> <span id="population"></span></p>
          <p><strong>Region:</strong> <span id="region"></span></p>
          <p><strong>Currency:</strong> <span id="currency"></span></p>
        </div>
      </div>
    </section>
  </body>

Now for the CSS, let's apply some basic styling while initially hiding the result section.

.container {
  width: 400px;
  margin: 50px auto;
  text-align: center;
  font-family: Arial, sans-serif;
}

.search-box {
  margin-bottom: 20px;
}

#country-input {
  width: 70%;
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 4px;
}

#search-btn {
  padding: 10px;
  cursor: pointer;
  border: none;
  border-radius: 4px;
  background-color: rgb(68, 66, 213);
  color: #f9f9f9;
}

.country-info {
  background-color: #f9f9f9;
  padding: 20px;
  border-radius: 10px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

#country-flag {
  width: 100px;
  margin: 10px 0;
}

#error-message {
  color: red;
  font-weight: bold;
  margin: 10px 0;
}

#result {
  display: none;
}

The #result is initially hidden using display: none; to prevent the country information from being shown when the page loads or before the user performs a search. The JavaScript code updates the content once a user searches for a country and valid data is fetched from the API. It makes the #result visible by changing its display property, ensuring the user only sees it after a successful search.

The web app doesn't work because the JavaScript hasn't been implemented.

Making a Basic GET Request

In this section, we’ll make a GET request to an external API, display the fetched country information, and log them to the console.

  // Function to fetch country data from an external web API
const fetchCountryData = (country) => {
  fetch(`https://restcountries.com/v3.1/name/${country}`)
    .then((response) => response.json())
    .then((data) => console.log(data));
};
fetchCountryData('France'); //calling function

This code snippet defines a function, fetchCountryData, that retrieves information about a country. It takes a single argument, country, which specifies the desired nation.

Here's a breakdown of its functionality line by line for better understanding:

const fetchCountryData = (country) => { ... }

This line establishes a function named fetchCountryData that accepts one parameter, country. The arrow (=>) signifies it's a concise arrow function.

fetch("https://restcountries.com/v3.1/name/${country}")

This line utilizes the Fetch API to initiate a request to the external API at https://restcountries.com. The URL cleverly includes a placeholder, ${country}, which will be dynamically replaced with the actual country name you provide.

The entire fetch call is wrapped in a chain of .then methods. These methods handle the asynchronous nature of the request smoothly.

.then((response) => response.json())

This line of code deals with the response received from the API. It verifies if the response is successful and subsequently transforms the response body (potentially in JSON format) into a JavaScript object using .json().

.then((data) => console.log(data))

This final .then method receives the parsed data, which holds the country information, as an argument named data. Here, it simply prints the data to the console for display using console.log.

fetchCountryData('France');

This line calls the fetchCountryData function and passes the string "France" as the argument. This starts the process of fetching data about France from the specified web API and logging the retrieved information to the console.

displays the fetch result in the console

In essence, this code snippet shows a simple way to fetch country data from an external API and display it in the console. It doesn't explicitly handle errors, but you can easily add a .catch block to manage any potential issues.

Handling Errors in GET Requests

When making GET requests, it's important to be ready for possible errors. These errors can happen due to network issues, server problems, or invalid requests. Adding good error-handling methods allows you to manage these situations smoothly and improve the user experience.

This section will explore different strategies for handling errors in GET requests, such as using the .catch method, checking response status codes, and providing informative error messages.

.then((response) => {
      if (!response.ok) {
        throw new Error('Country not found');
      }
      return response.json();

This code snippet above first checks if the server response is successful by evaluating response.ok. If the response indicates a failure (such as a 404 or 500 status code), the code throws an error using throw new Error('Country not found'). This stops further execution in the current .then() block and transfers control to the .catch() block for error handling. If the response is successful, it returns the data as JSON using response.json() for further processing.

The throw new Error() statement is used to generate an error in the code manually. In this case, it is triggered when the server responds with a non-OK status (such as an HTTP 404 error). When this error is thrown, it creates a custom error object with the message 'Country not found'.

This error then causes the promise to be rejected, skipping the remaining code in the .then() chain and moving directly to the .catch() block, where the error is handled (e.g., displaying an error message to the user). This ensures that failed requests are properly managed.

.catch((error) => {
      console.error('Error:', error.message);
    });

The .catch() block handles any error that occurs during the fetch request. If an error is thrown (like a failed network request or invalid response), the code inside .catch() is executed. In this example, it logs the error message to the console using console.error(), which helps identify issues in the request or response process.

The error.message property provides a human-readable description of the error that occurred. In the .catch() block, error.message is used to retrieve and display the specific reason for the error, such as "Country not found" or a network issue, making it easier to understand what went wrong during the fetch request.

Now that we understand how error handling works, let's update our code.

// Function to fetch country data from an external API
const fetchCountryData = (country) => {
  fetch(`https://restcountries.com/v3.1/name/${country}`)
    .then((response) => {
      if (!response.ok) {
        throw new Error('Country not found');
      }
      return response.json();
    })
    .then((data) => {
      console.log(data); // Log the fetched data for the specified country
      // You can process and use this data as needed
    })
    .catch((error) => {
      console.error('Error:', error.message); // Handle any errors during fetch
    });
};
fetchCountryData('France');

This code includes an error-handling function to ensure a smooth user experience and better code readability.

Get Country Info App

Now that we're familiar with the fetch API, let's refactor our code to make it more functional. We'll start by bringing in the elements from the HTML file.

// Get the HTML elements
const searchBtn = document.getElementById('search-btn'),
  countryInput = document.getElementById('country-input'),
  countryName = document.getElementById('country-name'),
  countryFlag = document.getElementById('country-flag'),
  capital = document.getElementById('capital'),
  population = document.getElementById('population'),
  region = document.getElementById('region'),
  currency = document.getElementById('currency'),
  resultDiv = document.getElementById('result'),
  errorMessage = document.getElementById('error-message');

Inside the fetchCountryData() function, just before making the GET request, let's get the user input and remove any extra spaces. We'll also set the error message and display to none. This helps handle any unexpected errors and clears previous error messages and results.

const country = countryInput.value.trim();
  if (country === ' ') {
    errorMessage.textContent = 'Please enter a country name';
    resultDiv.style.display = 'none';
    return;
  }
// Clear previous error messages and results
  errorMessage.textContent = ' ';
  resultDiv.style.display = 'none';

Next, we refactor the second .then() method that contains the result data. We get the necessary country details, assign them, and display them in the browser.

.then((data) => {
      const countryData = data[0]; // Get the first result

      // Get currency details (first currency listed)
      const currencyCode = Object.keys(countryData.currencies)[0];
      const currencyName = countryData.currencies[currencyCode].name;
      const currencySymbol = countryData.currencies[currencyCode].symbol;

      // Display the country information
      countryName.textContent = countryData.name.common;
      countryFlag.src = countryData.flags.png;
      capital.textContent = countryData.capital[0];
      population.textContent = countryData.population.toLocaleString();
      region.textContent = countryData.region;
      currency.textContent = `${currencyName} (${currencySymbol})`;
      resultDiv.style.display = 'block'; // Show the results
    })

This block of code above processes the fetched country data and updates the webpage with specific details. It initially extracts the first country result from the data array, then retrieves the currency information by finding the first currency listed and obtaining its name and symbol.

Next, it updates the HTML elements to display the country's name, flag, capital, population, region, and currency. Finally, it makes the result section visible by changing its display style, allowing the user to see the fetched information.

.catch((error) => {
      errorMessage.textContent = 'Country not found. Please try again.';
      resultDiv.style.display = 'none'; // Hide results if an error occurs
    });

Let's update the error handling code with the code above. If an error occurs, it updates the error message element to display "Country not found. Please try again." Additionally, it hides the result section by setting its display style to none, ensuring that no incorrect data is shown when an error occurs.

Now we add an event listener to the button, passing the callback function fetchCountryData as an argument to ensure it works as planned.

// Add event listener to the search button
searchBtn.addEventListener('click', fetchCountryData);

Here we used fetchCountryData without parentheses in searchBtn.addEventListener('click', fetchCountryData); because you're passing the function itself as an argument to the addEventListener method.

If you used fetchCountryData() with parentheses, you would be calling the function right away and passing the result to addEventListener. This is not what you want, as you want the function to be called only when the button is clicked.

This is the updated code and output. You can test it below:

For mobile: best viewed with 0.5x.

Working with Async/Await in Fetch API

async/await is a modern approach to managing asynchronous tasks in JavaScript. While the .then() method is good for chaining promises, it can make the code harder to read and manage, especially with multiple asynchronous operations.

async/await simplifies working with promises by letting you write asynchronous code that looks and acts more like synchronous code. It removes the need for chaining .then() methods, making the code easier to read and follow.

With async, you declare that a function will handle asynchronous operations. By using await within that function, you pause its execution until a promise is resolved. This makes the code more readable and reduces nesting, especially when dealing with multiple asynchronous tasks.

To declare an asynchronous function with the fetch API, use the async keyword before the function declaration. This shows that the function will handle asynchronous operations using promises.

The await keyword is exclusively used within asynchronous functions. When you place await before a promise, the function's execution will pause until the promise is resolved.

For example:

async function fetchData() { 
const response = await fetch('https://api.example.com/data'); 
const data = await response.json(); 
return data;
}

In this example, the fetchData() function is defined as asynchronous using async. The await keyword pauses the function until the fetch request is completed and the response is converted to JSON. This makes the code simpler compared to using .then() chains.

Error Handling with Async/Await

In async/await functions, errors can be handled using a try...catch block. This structure allows you to catch any expectations that might occur during the execution of the asynchronous code. If an error is encountered, the catch block will be executed, providing an opportunity to handle the error gracefully. try...catch blocks can make your asynchronous functions more robust and resilient to potential errors.

Example:

async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Error fetching data:', error.message);
  }
}

Note: catch in try…catch is a block that automatically catches any error thrown inside the try block, including await operations. The catch block doesn’t need an arrow function because it is not a callback or a function itself—it's just a control structure that will execute when an error is thrown.

Conclusion

Congratulations on reaching this milestone—your new skill with the fetch API unlocks endless possibilities for creating dynamic, data-driven web experiences! Mastering the fetch API is a big achievement in modern web development. With its simplicity and power, fetch changes how we handle HTTP requests, providing a more streamlined solution than older methods like XMLHttpRequest.

Using fetch alongside async…await and robust error handling, you can now build more efficient, responsive, and reliable applications. Feel free to refactor the code we created using the async…await functions. In the next article, we’ll explore the POST method, headers, and the common pitfalls to avoid when using the fetch API.

More Resources

Happy Coding! ✨

29
Subscribe to my newsletter

Read articles from Graham Boyle directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Graham Boyle
Graham Boyle

A Front-end Engineer and Technical Writer