Handling async errors with Axios in React

Have you ever been stuck on what looks like an empty page, and you ask yourself, "Am I supposed to be seeing something yet?", only for it to appear like 30 seconds later? Or maybe you've clicked on a button and you're not sure whether its processing or not (like on checkout pages). If thats what your own app feels like then, read on. In this guide, I'll walk you through 4 scenarios you should handle when working with APIs using axios & react.

  • Handling requests that sometimes take longer than usual and leave the user looking at an empty page
  • Handling requests that have errored and you want to give the user a way out
  • Handling a possible timeout where the request is taking significantly longer than usual and giving the user an updated loading message so they see the page isn't frozen
  • Handling a definite timeout where you want to abort the request to give the user a more specific error message

I'll start with a very innocent example - When the ResultsList component loads, it request some data, and displays it in the UI.

const ResultsList = () => {
  const [results, setResults] = useState([])

  // run on load
  useEffect(() => {
    axios.get(apiUrl).then(response => {
      setResults(response.data)
    }).catch(err => {
      console.log(err)
    })
  }, [])

  return (
    <ul>
      { results.map(result => {
          return <li>{result.name}</li>
        })
      }
    </ul>
    )
  }

The data from the API is being stored in the results field of the component's state. It starts as an empty array, and gets replaced with the actual results when the data is fetched.

This is buggy. Here's why.

1. Handling long response times

Not all users will have a great connection, even if your backend is stable, and that request may take a little longer to complete than usual. In this example, there's no feedback to the user that something is happening, and when there are issues, the page will be empty for a lot longer than you're expecting.

It will make your users ask, "Am I supposed to be seeing something yet?"

This question can be solved with a few snippets:

const ResultsList = () => {
+  const [results, setResults] = useState(null)

...

+  const getListItems = () => {
+    if(results) {
+      return results.map(result => {
+        return <li>{result.name}</li>
+    })
+    } else {
+      return (
+        <div>
+          <i class="fas fa-spinner fa-spin"></i>
+          Results are loading...
+       </div>
+      )
+    }
+   }
+
+  return (
+    <div>
+      <ul>{getListItems()}</ul>
+    </div>
+    )
  }

There are 2 changes

  • Rather than initialize results to an empty array [], its initialized to null.
  • I can then check if I should show a loading message, or if I should show the list with data.

Pro-tip: Add spinners. They're so easy. And fun. Your users will be less confused. I say this as someone who was too lazy to add spinners.

2. Handling errors

When something goes wrong, the easy options are to write it to the console, or the show user an error message. The best error message is one that can tell the user how to fix whatever just happened. I have more details on the axios error object on this post here

The easy option is to show the user something useful on any error, and offer them a way to fix things.

const ResultsList = () => {
  const [results, setResults] = useState(null)
+ const [error, setError] = useState(null)

+ const loadData = () => {
+   return axios.get(apiUrl).then(response => {
+     setResults(response.data)
+     setError(null)
+   }).catch(err => {
+     setError(err)
+   })
+ }

  // run on load
  useEffect(() => {
+   loadData()
-    ...
  }, [])

+ const getErrorView = () => {
+   return (
+     <div>
+       Oh no! Something went wrong.
+       <button onClick={() => loadData()}>
+         Try again
+.      </button>
+     </div>
+   )
+ }

  return (
    <div>
+    <ul>
+      {  error ? 
+        getListItems() : getErrorView() 
+      }
+    </ul>
-    <ul>{getListItems()}</ul>
    </div>
    )
  }

Here, I've added an error field to the component's state where I can keep track of errors and conditionally show an error message to the user. I give them a painfully generic error message, but I offer a way out via a button to Try again which will retry the request.

When the request succeeds, the error is cleared from the state, and the user sees the right info. Wonderful.

3. Handling a possible timeout

You get extra bonus points if you add this. Its not that its hard, its just that its quite considerate. Every once in awhile, a user will come across a loading spinner, and they'll wonder - "Is it frozen and the icon is just spinning or does it really take this long?"

If a request is taking awhile, you can give the user a little feedback in the form of, "This is taking longer than usual..."

+ const TIMEOUT_INTERVAL = 60 * 1000

const loadData = () => {
+   if (results) setResults(null)

    // make the request
    axios.get(apiUrl).then(response => {
      setResults(response.data)
      setError(null)
    }).catch(err => {
      setError(err)
    })

+   // show an update after a timeout period
+   setTimeout(() => {
+     if (!results && !error) {
+       setError(new Error('Timeout'))
+     }
+   }, TIMEOUT_INTERVAL)
}

const getErrorView = () => {
+   if (error.message === 'Timeout') {
+     <div>This is taking longer than usual</div>
+   } else {
      return (
        <div>
          Oh no! Something went wrong.
          <button onClick={() => loadData()}>
           Try again
          </button>
        </div>
      )
+    }
}

You can set a timer for whichever TIMEOUT_INTERVAL you want. When the timer executes, check whether the data has already loaded, and show the extra error message. If there are no results yet, and no errors, we're still waiting for data so you can show the updated loading message.

4. Handling a definite timeout

If you know a request is supposed to be quick, and you want to give the user quick feedback, you can set a timeout in the axios config.

const loadData = () => {
    if (results) setResults(null)

    // make the request
+    axios.get(apiUrl, { timeout: TIMEOUT_INTERVAL }).then(response => {
-    axios.get(apiUrl).then(response => {
      setResults(response.data)
      setError(null)
    }).catch(err => {
      setError(err)
    })
        ...
}
const getErrorView = () => {
    if (error.message === 'Timeout') {
      <div>This is taking longer than usual</div>
+    } else if (error.message.includes('timeout')) {
+      <div>This is taking too long. Something is wrong - try again later.</div>
+    } else {
      return ...
    }
}

Axios will throw an error here when the request rejects and it'll have an error message like timeout of 30000ms exceeded

Get started

Take a look at your React projects that are working with APIs and review how you're handling errors. These tips are an easy way to give your users with a more resilient experience. If you've tried it out or have different ways of approaching your errors, let me know in the comments!

Tags:

You might be interested in…

Menu