Handle HTTP Errors with React

Kent C. Dodds
InstructorKent C. Dodds

Share this video with your friends

Send Tweet
Published 2 years ago
Updated 10 months ago

Unfortunately, sometimes a server request fails and we need to display a helpful error message to the user. In this lesson we’ll handle a promise rejection so we can collect that error information, and we’ll also learn how we can best display manage the state of our request so we have a deterministic render method to ensure we always show the user the proper information based on the current state of our React component.

A common mistake people make is to create a state variable called isLoading and set that to true or false. Instead, we’ll be using a status variable which can be set to idle, pending, resolved, or rejected. You can learn more about why this is important from Stop using isLoading booleans.

Instructor: [0:00] Now what would happen if there was some sort of server error or maybe we made the request incorrectly? Let's take a look here.

[0:06] Let's go down to our query. We'll make a typo. We'll say, "Nam" instead of "Name." We'll save that.

[0:12] When I type in here, I can say, "Mew" and then submit. As a user, I'm just going to see this "...". That's not going to be useful at all. Let's see what's going on here in our developer tools. I'll go ahead and refresh.

[0:26] We'll type Mew again. Let's clear out our network tab. Then we hit submit. We're going to get a network error indicating that we cannot query field nam on type Pokémon. It gives a suggestion for, "Did you mean name?"

[0:41] The specific error is beside the point. We just need to show the user something a little bit more useful than leaving them in a loading state forever.

[0:49] Let's come back up here before we fix our code. Let's add some state for the error state. We'll say setError. We'll initialize that to null. Then we'll say if there's an error, then we'll return, "Oh, no..."

[1:06] In a real application, maybe you'd be a little bit more helpful than that. Let's add an error handler here as a second argument to our then call. This will be our error data.

[1:17] We'll say setError with the error data. Then we could submit that. We'll type the Pokémon name again. We see, "Oh, no." Now we need to try again.

[1:29] The problem that we'll face now is that if the user does try again and the server is successful, the way that we have our code structured is not going to work with that. What I'm going to do is I'm going to add a new state here for status.

[1:42] We'll have setStatus. We'll initialize that status to idle, meaning right now the Pokemon info is not doing anything useful.

[1:51] Then we can say that the idle status is when we want to render submitAPokemon. If the status is idle, then we'll say, "submitAPokemon." When we start fetching a new Pokémon, then we can say, "setStatus pending."

[2:10] Then we can have this represent if the status is pending, then we want to return a "..." to indicate to the user that we're pending. When we have a successful request, then we can say, "setStatus to resolved."

[2:26] If we're resolved, then we want to return the Pokémon data. If the status is resolved, then we'll return the Pokémon data in a pre tag here.

[2:38] Then if there's an error, we'll say, "setStatus rejected." Down here, we'll say, "Status is rejected." Then we'll render, "Oh, no."

[2:50] Now our render method is very predictable. We always know when our component is going to render what. If I try to type in Mew again, we're going to see, "Submit a Pokémon first."

[3:01] We'll see "..." while it's pending. Then we'll see "Oh, no." when it's been rejected.

[3:06] We'll fix our typo right here. We'll save that. Now we see "Submit a Pokémon" because we're idle. We'll type in Mew. Hit submit.

[3:15] We get that "..." Then we see Mew's details. If we try another Pokémon, then we'll see a "..." We'll see that Pokémon's details.

[3:25] Let's go ahead and review. We added some error handling by creating some error state management. We added an error handler to our promise chain. If we get some error data, then we're going to set that error data so that we can render something useful to the user indicating that there's been a problem.

[3:42] To avoid some state bugs, we added a status state so that we could start out with idle. When we start fetching the Pokémon, we can set it to pending. When we get the Pokémon, we can set it to resolved, or if there's a failure in getting the Pokémon, then we set it to rejected. That helps us to avoid bugs.

Stephen James
Stephen James
~ 2 years ago

I notice that you didn't use .catch for you promise in useEffect. Is this a style you prefer? Is there an advantage?

Kent C. Dodds
Kent C. Doddsinstructor
~ 2 years ago

Yes, there is an advantage:

promise.then(
  () => {
    throw new Error('oh no')
  },
  () => { /* not called */ }
).catch(() => { /* called because the success handler failed */ })

In my example, I only want to catch errors with the promise itself, not with the success handler.

Quang Le
Quang Le
~ 2 years ago

It is very nice that you show how to make an async request and handle errors from a React component. In my opinion, this kind of pattern is commonly used to interact with remote server and I wonder if there is any built-in component or third party component library to simplify this process? Thank you.

Kent C. Dodds
Kent C. Doddsinstructor
~ 2 years ago

Take a look at react-query

Dimitar Danailov
Dimitar Danailov
~ 2 years ago

@kent the course is really good and useful.

I want to share my comment how the source code can be better. The source code implementation is:

const [status, setStatus] = React.useState('idle')

The native React hook can be replaced with state machine: https://xstate.js.org/viz/

Kyle Shevlin has a great course: Introduction to State Machines Using XState

The state machine can be:

const fetchMachine = Machine({
    id: 'fetch',
    initial: 'idle',
    context: {
      retries: 0
    },
    states: {
      idle: {
        on: {
          FETCH: 'loading'
        }
      },
      loading: {
        on: {
          RESOLVE: 'success',
          REJECT: 'failure'
        }
      },
      success: {
        type: 'final'
      },
      failure: {
        on: {
          RETRY: {
            target: 'loading',
            actions: assign({
              retries: (context, event) => context.retries + 1
            })
          }
        }
      }
    }
  })
Dmitry
Dmitry
~ a year ago

Yes, I think it is worth mentioning that this approach of handling the request status - is the state machine and basically every network request should be done using such an approach.