Skip to main content

Welcome to a Grasp lesson

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

Lesson 33

Robust Error Handling with Try/Catch

Recommended resources

Hello! Welcome back to our course.

In the last lesson, we built a solid foundation for working with APIs. You learned how to use the modern async/await syntax to fetch data and how to implement a basic error-handling pattern using try...catch and response.ok. We successfully fetched data, but our error handling was developer-focusedβ€”we only logged errors to the console.

Today, we'll build directly on that knowledge. Our goal is to handle API errors gracefully using try/catch blocks and display user-friendly messages. A user doesn't have the developer console open, so a silent failure or a frozen interface creates a poor experience. By the end of this lesson, you'll be able to communicate clearly to the user what's gone wrong and what they can do about it, a crucial skill for building professional web applications.

1. The Two Types of Fetch Errors

When making a network request, things can go wrong in a couple of distinct ways. As we briefly touched on before, it's vital to understand the difference because fetch treats them differently.

  • Network/Connection Errors: These happen when the request can't even reach the server. This could be due to the user being offline, a DNS issue, a CORS policy violation, or a typo in the domain name. The fetch() promise will reject in this case, and the error will be caught by the catch block.
  • HTTP Status Errors: These occur when the request successfully reaches the server, but the server responds with an error status. Common examples include 404 Not Found (the requested resource doesn't exist) or 500 Internal Server Error (something went wrong on the server). The fetch() promise will resolve successfully because a response was received. This is a common point of confusion.

This distinction is the single biggest "gotcha" when handling errors with fetch. Let's see a clear demonstration of this.

JavaScript Fetch API - One Mistake I ALWAYS MAKE!

The video 'JavaScript Fetch API - One Mistake I ALWAYS MAKE!' by James Q Quick perfectly illustrates this common pitfall. Notice how a 404 error does not trigger the catch block.

Watch from 00:51 to 02:08, and then from 03:18 to 03:55. In the first segment, observe how a 404 error doesn't go to the catch block and instead causes a different error when the code tries to parse the non-JSON response. The second segment shows what kind of error (an invalid domain) does trigger the catch block.

To solidify this concept, the following article provides a great textual breakdown of the same problem.

Fetch API, do you really know how to handle errors? - DEV Community

The article 'Fetch API, do you really know how to handle errors?' reinforces what we just saw. It clearly explains which errors are caught by try/catch and which are not.

Read the introduction and the sections 'How to handle errors using Fetch API?' and 'Using try/catch to handle Fetch API errors'. Focus on the distinction between promise rejection (network/CORS errors) and promise resolution with a bad status (404/500 errors).


2. A Robust, Unified Error-Handling Strategy

So, if try...catch only handles network errors, how do we create a single, clean way to manage all possible failures? The best practice is to combine try...catch with an explicit check of the response status, and then make our own error to unify the logic.

The strategy looks like this:

  1. Wrap the await fetch(...) call in a try...catch block. This will handle any network errors.
  2. Inside the try block, right after getting the response, check if response.ok is false.
  3. If it is false, throw a new Error. This will immediately stop the execution of the try block and jump to the catch block.

This way, the catch block becomes the single source of truth for handling any kind of error, whether it was a network failure or a bad HTTP status.

Try-Catch-Finally Flowchart
This flowchart shows the execution path of a `try...catch` block. If an error occurs in the `try` block (either from a network failure or from our own `throw` statement), control immediately passes to the `catch` block. If no error occurs, the `catch` block is skipped entirely.

The following resource shows this exact pattern in action.

Fetch API, do you really know how to handle errors? - DEV Community

Let's return to the DEV Community article, which presents this unified solution beautifully.

Read the section 'Best way to handle errors using Fetch API', paying close attention to Example 02. This code demonstrates how to check response.ok, throw a new custom error if needed, and handle all errors in one catch block.

This pattern gives us a predictable, maintainable structure for all our API calls.


3. Hands-On: Giving the User Proper Feedback

Now, let's apply this strategy to the PokΓ©-Fetcher you built in the last lesson. Our goal is to stop logging errors to the console and start showing them in the UI.

User Feedback Modals: Success and Error States
Good user interfaces provide clear feedback for both success and failure. These modal dialogs are an example of clear, user-friendly messages that help the user understand what happened.

We'll implement a simpler version of this by adding a dedicated error message area to our page.

Your Task: Upgrade the PokΓ©-Fetcher

Start with your code from the previous lesson. If you don't have it handy, use this as your starting point.

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>PokΓ©-Fetcher</title>
  <style>
    body { font-family: sans-serif; text-align: center; margin-top: 50px; }
    input { padding: 8px; }
    button { padding: 8px 12px; }
    img { margin-top: 20px; width: 150px; height: 150px; border: 1px solid #ccc; }
    /* Style for our new error message */
    .error { color: red; margin-top: 15px; }
  </style>
</head>
<body>
  <h1>PokΓ©-Fetcher</h1>
  <input type="text" id="pokemonName" placeholder="Enter PokΓ©mon name">
  <button onclick="fetchData()">Fetch PokΓ©mon</button>
  
  <!-- Add a paragraph to display errors -->
  <p id="errorMessage" class="error"></p>

  <br>
  <img src="" alt="Pokemon Sprite" id="pokemonSprite" style="display: none;">

  <script src="app.js"></script>
</body>
</html>

app.js (Starter Code)

// This is the function you will modify
async function fetchData() {
    const pokemonName = document.getElementById("pokemonName").value.toLowerCase();
    
    try {
        const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${pokemonName}`);
        
        // This is where the old, simple check was.
        if (!response.ok) {
            console.error(`HTTP error! Status: ${response.status}`);
            return; // We will improve this!
        }

        const data = await response.json();
        const spriteUrl = data.sprites.front_default;
        
        const pokemonSpriteElement = document.getElementById("pokemonSprite");
        pokemonSpriteElement.src = spriteUrl;
        pokemonSpriteElement.style.display = "block";

    } catch (error) {
        console.error("Fetch error:", error);
    }
}

Instructions:

  1. Modify index.html: Add the <p id="errorMessage" class="error"></p> element as shown above. This is where we'll display our messages.

  2. Refactor app.js:

    • At the beginning of your fetchData function, get a reference to the error message p tag and the img tag. It's a good practice to clear any previous error message and hide the image at the start of a new fetch.
    • Inside the try block, replace the old if (!response.ok) block. Instead of just logging to the console, throw new Error(...). Create a user-friendly message, like throw new Error(\Could not find: ${pokemonName}`);. You can even include the status code: throw new Error(`Error ${response.status}: PokΓ©mon not found`);`.
    • Update your catch (error) block. Instead of logging to the console, set the textContent of your error message element to error.message. Make sure the error message is visible.

Test your work by trying to fetch a PokΓ©mon that exists (like "pikachu") and one that doesn't (like "fakemon"). Also, try disconnecting from the internet to simulate a network error. Both types of errors should now display a message on the page.

Click to see the completed `app.js` file
async function fetchData() {
    const pokemonName = document.getElementById("pokemonName").value.toLowerCase();
    const errorMessageElement = document.getElementById("errorMessage");
    const pokemonSpriteElement = document.getElementById("pokemonSprite");

    // 1. Reset UI state on new fetch
    errorMessageElement.textContent = "";
    pokemonSpriteElement.style.display = "none";

    // Stop if the input is empty
    if (!pokemonName) {
        errorMessageElement.textContent = "Please enter a PokΓ©mon name.";
        return;
    }

    try {
        const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${pokemonName}`);
        
        // 2. Check for HTTP errors and throw to centralize error handling
        if (!response.ok) {
            throw new Error(`Error ${response.status}: Could not find PokΓ©mon "${pokemonName}"`);
        }

        const data = await response.json();
        const spriteUrl = data.sprites.front_default;
        
        // On success, display the image
        pokemonSpriteElement.src = spriteUrl;
        pokemonSpriteElement.style.display = "block";

    } catch (error) {
        // 3. Catch ALL errors (network or thrown) and display in the UI
        console.error(error); // It's still good practice to log the full error for debugging
        errorMessageElement.textContent = error.message;
    }
}

Conclusion

Great job! You have now elevated your error-handling skills from being developer-centric to user-centric. This is a significant step towards building professional, resilient applications.

Key Takeaways:

  • fetch has two main error types: network errors (promise rejects) and HTTP status errors (promise resolves).
  • The try...catch block only catches rejected promises (network errors) by default.
  • A robust pattern is to check response.ok inside the try block and throw new Error() on failure. This allows the catch block to handle all errors uniformly.
  • Always provide clear, user-friendly feedback in the UI when an error occurs. A silent failure is a frustrating user experience.

In our next lesson, we will focus on what to do with a successful API response. You'll learn how to parse a JSON response and render the fetched data dynamically into the DOM, which will allow us to build much more complex and interactive features, like displaying a list of items fetched from an API.