Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 65

Handling API Loading and Error States

Recommended resources

Hello! In our last lesson, we laid the groundwork for connecting your React application to external APIs by fetching data with useEffect. We successfully retrieved data, but our component doesn't give the user any feedback while the request is happening or if something goes wrong.

This lack of feedback can lead to a confusing user experience. A user might see a blank screen and wonder if the app is broken, or they might not know that the data they're seeing is stale because a new request failed silently.

Today, we'll solve this by learning how to manage loading and error states during an API request. By the end of this lesson, you will be able to provide clear, immediate feedback to your users, making your application feel more professional and robust.

The general flow we're aiming for looks like this:

Handling Loading and Error States in API Requests
This diagram shows the journey of an API request. The client initiates it, enters a 'Loading' phase, and then arrives at either a 'Successful' data display or an 'Error' message.

1. Implementing the Loading State

The first step is to inform the user that data is being fetched. We can achieve this by adding a loading state to our component.

The pattern is straightforward:

  1. Create a new state variable, let's call it isLoading, and initialize it to true or false.
  2. Just before you make the API call, set isLoading to true.
  3. Once the request is complete (whether it succeeds or fails), set isLoading back to false.
  4. In your JSX, use this isLoading flag to conditionally render a loading indicator, like a simple "Loading..." message or a spinner component.

This video provides an excellent explanation and demonstration of why a loading state is crucial for user experience and how to implement it.

Fetching Data in React - Complete Tutorial

Watch this section from "Fetching Data in React - Complete Tutorial" by Cosden Solutions. It clearly explains the need for a loading indicator and shows how to implement it using a useState hook.

Watch from 06:30 to 09:48. Pay attention to: The reasoning behind why a loading state improves user experience. How a new isLoading state variable is created. Where setIsLoading(true) and setIsLoading(false) are called in the data-fetching function. How conditional rendering is used to show a 'Loading...' message.

As you saw, the logic is quite simple but has a major impact. For a quick text-based summary of the code structure, you can review the following article.

How to fetch data with React Hooks - Robin Wieruch

This article from Robin Wieruch's blog provides a concise code example of adding a loading indicator. It's a great way to solidify your understanding of the code structure.

Read the section titled "Loading Indicator with React Hooks". Focus on the addition of the isLoading state and the conditional rendering logic within the JSX.


2. Handling API Errors

Now, let's tackle what happens when things go wrong. When using the fetch API, there are two main categories of errors you need to handle:

  1. Network Errors: These occur when the request can't even reach the server. This could be due to a loss of internet connectivity, a DNS issue, or the server being completely offline. The fetch promise will reject in this case, which can be caught by a .catch() block or a try...catch statement.
  2. HTTP Errors: These occur when the server does send a response, but it's an error response (e.g., status code 404 Not Found or 500 Internal Server Error). Surprisingly, fetch does not consider these as errors. The promise will resolve successfully. It's up to you to check the response.ok property (which is true for statuses 200-299) and handle it.

The process for managing errors mirrors how we handled the loading state:

  1. Create an error state variable, initialized to null.
  2. Use a try...catch block around your fetching logic.
  3. Inside the try block, check if (!response.ok) and throw your own error if it's not.
  4. In the catch block, update your error state with the error object.
  5. Conditionally render an error message in your JSX if the error state is not null.

The following video is one of the clearest demonstrations of this concept. The instructor simulates both a network error (by stopping the server) and an HTTP error (by using a bad endpoint) to show how to catch both.

Full React Tutorial #19 - Handling Fetch Errors

This tutorial from The Net Ninja, "Full React Tutorial #19 - Handling Fetch Errors", expertly demonstrates how to handle both network and server-side errors.

Watch the entire video (00:00 to 07:39). The key takeaways are: 00:00 - 01:51: Using a .catch() block to handle network connection errors. 01:51 - 05:17: Checking the response.ok property to identify server errors and throwing a new error to be caught. 05:17 - 07:39: Storing the error in a state variable and conditionally rendering it. Notice how he also sets isPending (his loading state) to false within the error handler.

This clear distinction between the two error types is fundamental to writing robust data-fetching code in React.


3. Putting It All Together: A Robust Fetch Component

Let's combine everything we've learned into a single, robust component. Here is the UserList component from our last lesson, now upgraded with proper loading and error handling.

Notice the finally block. A try...catch...finally statement ensures that the code in the finally block runs regardless of whether the try block succeeded or an error was caught. This makes it the perfect place to set isLoading to false, as we want to stop showing the loading indicator in both success and error scenarios.

import { useState, useEffect } from 'react';

function UserList() {
  const [users, setUsers] = useState([]);
  const [isLoading, setIsLoading] = useState(true); // Start as true since fetch runs on mount
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUsers = async () => {
      try {
        const response = await fetch('https://jsonplaceholder.typicode.com/users');
        
        // Check for HTTP errors
        if (!response.ok) {
          throw new Error(`HTTP error! Status: ${response.status}`);
        }
        
        const data = await response.json();
        setUsers(data);
      } catch (e) {
        // This will catch network errors and the errors we throw
        setError(e);
      } finally {
        // This runs regardless of success or failure
        setIsLoading(false);
      }
    };

    fetchUsers();
  }, []);

  // Conditional rendering based on state
  if (isLoading) {
    return <p>Loading users...</p>;
  }

  if (error) {
    return <p>An error occurred: {error.message}</p>;
  }

  return (
    <div>
      <h1>User List</h1>
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}

export default UserList;

This three-state approach (data, loading, error) is a fundamental pattern in React for handling asynchronous operations.


Conclusion

You've now elevated your data-fetching skills significantly. By managing loading and error states, you're not just getting data; you're building a resilient and user-friendly experience.

Key Takeaways:

  • Three States of Data Fetching: A robust component fetching data needs to manage three pieces of state: the data itself, a loading boolean, and an error object.
  • Provide User Feedback: Use the loading state to show an indicator while waiting for the response and the error state to display a helpful message if the request fails.
  • Handle All Error Types: Remember to use a try...catch block for network errors and to explicitly check response.ok for HTTP errors when using fetch.
  • Use finally for Cleanup: The finally block is the ideal place to perform cleanup actions, like setting isLoading to false, because it executes after both successful and failed attempts.

Next Up:

We've mastered fetching data and handling the entire lifecycle of that request. The next logical step is to perform the other common API interactions: sending data to a server. In the next lesson, we will learn how to send data to an API using POST or PUT requests in response to user actions, such as submitting a form.