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:

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:
- Create a new state variable, let's call it
isLoading, and initialize it totrueorfalse. - Just before you make the API call, set
isLoadingtotrue. - Once the request is complete (whether it succeeds or fails), set
isLoadingback tofalse. - In your JSX, use this
isLoadingflag 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:
- 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
fetchpromise will reject in this case, which can be caught by a.catch()block or atry...catchstatement. - HTTP Errors: These occur when the server does send a response, but it's an error response (e.g., status code
404 Not Foundor500 Internal Server Error). Surprisingly,fetchdoes not consider these as errors. The promise will resolve successfully. It's up to you to check theresponse.okproperty (which istruefor statuses 200-299) and handle it.
The process for managing errors mirrors how we handled the loading state:
- Create an
errorstate variable, initialized tonull. - Use a
try...catchblock around your fetching logic. - Inside the
tryblock, checkif (!response.ok)and throw your own error if it's not. - In the
catchblock, update yourerrorstate with the error object. - Conditionally render an error message in your JSX if the
errorstate is notnull.
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
loadingboolean, and anerrorobject. - Provide User Feedback: Use the
loadingstate to show an indicator while waiting for the response and theerrorstate to display a helpful message if the request fails. - Handle All Error Types: Remember to use a
try...catchblock for network errors and to explicitly checkresponse.okfor HTTP errors when usingfetch. - Use
finallyfor Cleanup: Thefinallyblock is the ideal place to perform cleanup actions, like settingisLoadingtofalse, 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.