DEV Community

Leandro Coelho
Leandro Coelho

Posted on

User friendly errors with React error boundaries and fallback components

If we want to prevent our UI from crashing on errors and also have a fallback UI to show this errors in a friendly manner, we can use React error boundary components that wraps around critical parts of our app and catches JavaScript errors anywhere in it's child component tree.

Complete code example with typescript here.

Creating a custom error boundary component

Error boundaries are created as class components with access to two special lifecycle methods:

  • static getDerivedStateFromError() which updates it's state to show the fallback UI.
  • componentDidCatch() used to log error information.
class ErrorBoundary extends React.Component {
  state: State = {error: null}

  static getDerivedStateFromError(error) {
    return {error}
  }

  componentDidCatch(error, errorInfo) {
    logErrorToMyService(error, errorInfo);
  }

  render() {
    const {error} = this.state
    if (error) {
      return <this.props.FallbackComponent error={error} />
    }
    return this.props.children
  }
}
Enter fullscreen mode Exit fullscreen mode

In this example we are passing a FallbackComponent to be rendered if our ErrorBoundary catches an error and we are logging the error to a external service.

To use the ErrorBoundary component in our application we just need to wrap it around a component that might come across some errors. In this example I wrapped a component that fetches data from an API and passed a fallback component that shows an error message if something goes wrong:

<ErrorBoundary
  // use key as a workaround for resetting the errorboundary state
  key={circuitName}
  FallbackComponent={CircuitErrorFallback}
>
  <CircuitContent />
</ErrorBoundary>
Enter fullscreen mode Exit fullscreen mode
function CircuitErrorFallback({error}) {
  return (
    <div role="alert">
      <h3>Something went wrong...</h3>
      <p>{error.message}</p>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

The <CircuitContent /> component will throw an error if something goes wrong with our API call:

function CircuitContent({circuitName}) {
  const [state, setState] = useState<>({
    status: 'idle',
    circuit: {},
    error: null,
  })
  const {status, circuit, error} = state

  useEffect(() => {
    if (!circuitName) {
      return
    }
    setState(prevState => ({...prevState, status: 'pending'}))
    fetchCircuit(circuitName).then(
      circuit => {
        setState(prevState => ({...prevState, status: 'resolved', circuit}))
      },
      error => {
        setState(prevState => ({...prevState, status: 'rejected', error}))
      },
    )  
  }, [circuitName])

  if (status === 'idle') {
    return <CircuitIdle />
  } else if (status === 'pending') {
    return <CircuitLoading />
  } else if (status === 'rejected') {
    // throw error to be handled by error boundary
    throw error
  } else if (status === 'resolved') {
    return <CircuitDetails circuit={circuit} />
  }

  throw new Error('Something went really wrong.')
}
Enter fullscreen mode Exit fullscreen mode

And ErrorBoundary will catch this error and render our fallback component:

Error fallback component

Using react-error-boundary

Creating our own error boundary component is pretty straight forward but we can also install react-error-boundary package on our app and use it's features for resetting our error boundary and restoring the state of our UI.

import {ErrorBoundary} from 'react-error-boundary'

<ErrorBoundary
  onReset={handleReset}
  resetKeys={[circuitName]}
  FallbackComponent={CircuitErrorFallback}
>
  <CircuitContent circuitName={circuitName} />
</ErrorBoundary>
Enter fullscreen mode Exit fullscreen mode

Now we can extend our fallback component with a button for reset the errorboundary:

function CircuitErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div role="alert">
      <h3>Something went wrong...</h3>
      <p>{error.message}</p>
      <button onClick={resetErrorBoundary}>
        Try again
      </button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

And the resulting error UI will look like this:

Extended error fallback component

Conclusion

We can wrap different parts of our applications with error boundaries to keep our interface interactive and prevent crashing. This can also benefit us during development stage while catching errors that could even get unnoticed by typescript.

Note on usage with Create React App:

CRA may display an overlay with error information in development mode even if error boundary catches the error. There are workarounds to change this behavior of Create React App but I think it's unecessary, since you can press 'esc' to close the overlay and this will not be shown in production build anyway.

Tip for handling error messages with Axios:

Axios will throw an error with a custom message like "The server responded with 404 status code." when an API call fails. You can use an axios interceptor to change this custom message to the actual error message in the API response body or even map it to something else:

const api = axios.create({baseURL: 'https://api.backend.com'})
api.interceptors.response.use(
  response => response,
  error => {
    if (error.response.data.message) {
      error.message = error.response.data.message
    }
    return Promise.reject(error)
  },
)
Enter fullscreen mode Exit fullscreen mode

The idea for this post came from a lesson on the React hooks workshop from epicreact.dev. Thanks for reading!

Discussion (3)

Collapse
maniator profile image
Naftali Lubin • Edited

Consider using my npm module react-badly 😊

Collapse
frankfont profile image
Frank Font

I'd also suggest writing to the console with a little technical clue that might help on a support call --- more than you would display in the UI to a user but less than what a hacker would wish to see. dev.to/frankfont/oops-major-broker...

Collapse
kettanaito profile image
Artem Zakharchenko

Thank you for writing this, Leandro! Keep up the great work.