Welcome back! In our last lesson, you learned how to fetch data from an API and, crucially, how to interpret the JSON response to access the specific information you need. We successfully bridged the gap between a server's raw data and a usable JavaScript object.
This lesson addresses the next critical question from a user experience perspective: what does the user see in the meantime? An instantaneous response is rare, and a blank or frozen screen can make an application feel broken. Today, we will focus on implementing loading states to provide feedback during data fetching and empty states to gracefully handle cases where no data is returned. This is where your expertise as a UI/UX designer directly translates into code, allowing you to control the user's journey and manage their expectations.
The Importance of Feedback: Implementing Loading States
As a UI/UX designer, you know that perceived performance is often more important than actual performance. Showing a user that the system is working on their request is fundamental to a good experience. This is typically done with loading indicators like spinners or, more effectively, with skeleton screens.
The basic logic for implementing any loading state is the same:
- Before you start the
fetchrequest, you signal that the loading process has begun. - You display a placeholder (a spinner, a skeleton, or just text).
- Once the
fetchrequest completes (either with data or an error), you signal that loading is finished and hide the placeholder to show the final result.
Let's start with a simple text-based loading indicator. The following article demonstrates a straightforward way to achieve this using vanilla JavaScript.
Displaying loading animation on Fetch API calls
This article from DEV Community walks through the fundamental pattern of showing and hiding a loading element around a fetch call.
Read the introduction and the sections on HTML, CSS, and JavaScript. Pay close attention to how the displayLoading() and hideLoading() functions are called within the main fetchHandler() function. The loading element is shown before the fetch starts and hidden after it completes.
The article provides a solid starting point, but we can make it more robust. A try...catch block, which you learned about for error handling, can be combined with a finally block to ensure the loading indicator is always hidden, regardless of whether the fetch succeeds or fails.
// A more robust pattern
showLoadingIndicator();
try {
const response = await fetch(URL);
if (!response.ok) throw new Error('Network response was not ok');
const data = await response.json();
// ... display the data
} catch (error) {
// ... display an error message
} finally {
// This block runs no matter what, after the try or catch completes.
hideLoadingIndicator();
}
This try...catch...finally pattern is a cornerstone of professional frontend development. It ensures your UI doesn't get stuck in a permanent loading state if an error occurs.
A Better User Experience: Skeleton Screens
While a spinner provides feedback, a skeleton screen takes it a step further. It's a placeholder that mimics the layout of the content that is about to load. This creates the perception that the page is loading faster and tells the user what kind of content to expect.

As a designer, you've likely created these placeholders in Figma. The goal in code is to show this skeleton UI while the real data is being fetched.

The video below, while using React (which we'll start next month), brilliantly demonstrates the concept of building and using a skeleton loader. Focus on the "what" and "why," not the specific syntax for now.
Skeleton Loading in React Like a PRO!
This tutorial from Code Commerce is a fantastic preview of professional-grade loading states. It shows how to build a skeleton component that mirrors the final content card.
First, watch how the presenter builds the final Card component from this section. Then, watch how they create a corresponding Skeleton component that mimics its structure from this part, using gray shapes and a pulsing animation. Finally, see how they use a loading state to conditionally switch between the two from this point. This is the exact pattern you'll use to implement your own designs.
Handling Success with No Data: The Empty State
What happens if the API call is successful, but there's simply no data to show? For instance, a new user with no to-do items, or a search that yields no results. Displaying a blank area is a missed opportunity. A well-designed empty state confirms that the app is working correctly and can guide the user on what to do next.
The logic is simple: after the data has loaded successfully, and before you try to display it, you check if the data set is empty. For an array, this is as simple as checking if its length is 0.
This article provides a perfect, practical example within a vanilla JavaScript to-do application.
Build a CRUD Todo App with Vanilla JavaScript and Fetch API
This tutorial from "A Girl Codes" includes a clear implementation of an empty state when fetching a list of items.
Find the function displayTodos() within the "Displaying Todos in the DOM" section. Notice the if (todoList.length == 0) check. This is the key. If the array is empty, it injects HTML with a user-friendly message. If not, it proceeds to loop through the items and display them. This is the exact logic for handling an empty state.
Bringing It All Together: The Data-Fetching State Machine
You've now seen how to handle three critical scenarios: loading, success (with or without data), and errors. Thinking of these as a "state machine" is a powerful mental model that you'll use throughout your career.
At any given time, your component is in one of these states:
- Loading: An API request is in progress.
- Error: The request failed.
- Success: The request succeeded.
- Empty: The data returned is empty (e.g.,
data.length === 0). - Ideal: The data returned is ready to be displayed.
- Empty: The data returned is empty (e.g.,
The video below explains this entire flow using three state variables: isLoading, error, and the data itself. It's a masterclass in structuring asynchronous UI logic.
Fetching Data in React - Complete Tutorial
This video from Cosden Solutions provides a clear, step-by-step guide to managing the complete lifecycle of a data request, including loading and error states.
Watch the following three segments to see the full picture: Implementing the loading state: See how a isLoading variable is set to true before the fetch and false after. Implementing the error state: This reinforces the try...catch pattern for capturing errors and displaying a message. Using a finally block: This is a crucial refinement. It shows why placing setIsLoading(false) inside finally makes your code more reliable.
With this logic, you can provide your AI co-pilot with much more powerful prompts. Instead of just "fetch data and show it," you can describe the entire user experience:
"Create a function to fetch user profiles. While loading, show a skeleton UI. If an error occurs, display an alert message. If the fetch is successful but no profiles are returned, show a message that says 'No users found.' Otherwise, render the list of profiles."
Conclusion
In this lesson, you've moved beyond simply fetching data to managing the entire user experience around that process. By implementing loading and empty states, you are ensuring the user is never left confused or staring at a blank screen. This is a direct application of your UX skills in a coding context and is a hallmark of a great Design Engineer.
Key Takeaways:
- Always provide feedback during asynchronous operations. A simple loading state tells the user that the system is working.
- Skeleton screens are a superior loading pattern that manages user expectations and improves perceived performance.
- A successful API call with no data requires an empty state to confirm the app is working and to guide the user.
- The
try...catch...finallypattern is a robust way to handle the full lifecycle of a data request, ensuring your UI state is always correct. - Thinking in terms of states (Loading, Error, Success/Empty, Success/Data) allows you to build predictable and user-friendly interfaces.
In our next module, we will make a major leap. We'll leave vanilla JavaScript behind and enter the world of React. You'll see how concepts like components, state, and props allow us to build complex, interactive user interfaces in a much more organized and scalable way. The skeleton screen video gave you a small taste—get ready for the full experience