How to handle errors in React: full guide
Tags: react, webdev, javascript, tutorial
Published: 2023-02-23 07:00:00


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gns2u6eztxz97tvhuvfp.png)

Originally published at [https://www.developerway.com](https://www.developerway.com). The website has more articles like this 😉

---

We all want our apps to be stable, to work perfectly, and cater to every edge case imaginable, isn’t it? But the sad reality is we are all humans (at least that is my assumption), we all make mistakes, and there is no such thing as a bug-free code. No matter how careful we are or how many automated tests we write, there always will be a situation when something goes terribly wrong. The important thing, when it comes to user experience, is to predict that terrible thing, localize it as much as possible, and deal with it in a graceful way until it can be actually fixed.

So today, let’s take a look at error handling in React: what we can do if an error happens, what are the caveats of different approaches to error catching, and how to mitigate them.

## Why we should catch errors in React 

But first thing first: why it’s vitally important to have some error-catching solution in React?

The answer is simple: starting from version 16, an error thrown during React lifecycle will cause the entire app to unmount itself if not stopped. Before that, components would be preserved on the screen, even if malformed and misbehaved. Now, an unfortunate uncaught error in some insignificant part of the UI, or even some external library that you have no control over, can destroy the entire page and render an empty screen for everyone.

Never before had frontend developers such destructive power 😅

## Remembering how to catch errors in javascript

When it comes to catching those nasty surprises in regular javascript, the tools are pretty straightforward.

We have our good old `try/catch` [statement](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch), which is more or less self-explanatory: `try` to do stuff, and if they fail - `catch` the mistake and do something to mitigate it:

```tsx
try {
  // if we're doing something wrong, this might throw an error
  doSomething();
} catch (e) {
  // if error happened, catch it and do something with it without stopping the app
  // like sending this error to some logging service
}
```

This also will work with `async` function with the same syntax:

```tsx
try {
  await fetch('/bla-bla');
} catch (e) {
  // oh no, the fetch failed! We should do something about it!
}
```

Or, if we’re going with the old-school promises, we have a `catch` method specifically for them. So if we re-write the previous `fetch` example with promised-based API, it will look like this:

```tsx
fetch('/bla-bla').then((result) => {
  // if a promise is successful, the result will be here
  // we can do something useful with it
}).catch((e) => {
  // oh no, the fetch failed! We should do something about it!
})
```

It’s the same concept, just a bit different implementation, so for the rest of the article I’m just going to use `try/catch` syntax for all errors.

## Simple try/catch in React: how to and caveats

When an error is caught, we need to do something with it, right? So, what exactly can we do, other than logging it somewhere? Or, to be more precise: what can we do for our users? Just leaving them with an empty screen or broken interface is not exactly user-friendly.

The most obvious and intuitive answer would be to render something while we wait for the fix. Luckily, we can do whatever we want in that `catch` statement, including setting the state. So we can do something like this:

```tsx
const SomeComponent = () => {
  const [hasError, setHasError] = useState(false);

  useEffect(() => {
    try {
      // do something like fetching some data
    } catch(e) {
      // oh no! the fetch failed, we have no data to render!
      setHasError(true);
    }
  })

  // something happened during fetch, lets render some nice error screen
  if (hasError) return <SomeErrorScreen />

  // all's good, data is here, let's render it
  return <SomeComponentContent {...datasomething} />
}
```

We’re trying to send a fetch request, if it fails - setting the error state, and if the error state is `true`, then we render an error screen with some additional info for users, like a support contact number.

This approach is pretty straightforward and works great for simple, predictable, and narrow use cases like catching a failed `fetch` request.

But if you want to catch all errors that can happen in a component, you’ll face some challenges and serious limitations.

**Limitation 1: you will have trouble with useEffect hook.**

If we wrap `useEffect` with `try/catch`, it just won’t work:

```tsx
try {
  useEffect(() => {
    throw new Error('Hulk smash!');
  }, [])
} catch(e) {
  // useEffect throws, but this will never be called
}
```

It’s happening because `useEffect` is called asynchronously after render, so from `try/catch` perspective everything went successfully. It’s the same story as with any [Promise](https://www.developerway.com/posts/fetching-in-react-lost-promises#part1): if we don’t wait for the result, then javascript will just continue with its business, return to it when the promise is done, and only execute what is _inside_ `useEffect` (or then of a Promise). `try/catch` block will be executed and long gone by then.

In order for errors inside `useEffect` to be caught, try/catch should be placed _inside_ as well:

```tsx
useEffect(() => {
 try {
   throw new Error('Hulk smash!');
 } catch(e) {
   // this one will be caught
 }
}, [])
```

Play around with this example to see it:

{% embed https://codesandbox.io/embed/try-catch-and-useeffect-28h3ux?file=/src/App.tsx?codemirror=1&hidedevtools=0&view=preview %}


This applies to any hook that uses `useEffect` or to anything asynchronous really. As a result, instead of just one `try/catch` that wraps everything, you’d have to split it into multiple blocks: one for each hook.

**Limitation 2: children components.** try/catch won’t be able to catch anything that is happening inside children components. You can’t just do this:

```tsx
const Component = () => {
  let child;

  try {
    child = <Child />
  } catch(e) {
    // useless for catching errors inside Child component, won't be triggered
  }

  return child;
}
```

or even this:

```tsx
const Component = () => {
  try {
    return <Child />
  } catch(e) {
    // still useless for catching errors inside Child component, won't be triggered
  }
}
```

Play around with this example to see it:

{% embed https://codesandbox.io/embed/try-catch-for-children-doesnt-work-5elto1?file=/src/App.tsx?codemirror=1&hidedevtools=0&view=preview %}

This is happening because when we write `<Child />` we’re not actually rendering this component. What we’re doing is creating a component `Element`, which is nothing more than a component’s _definition_. It’s just an object that contains necessary information like component type and props, that will be used later by React itself, which will actually trigger the render of this component. And it will happen _after_ `try/catch` block is executed successfully, exactly the same story as with promises and useEffect hook.

If you’re curious to learn in more detail how elements and components work, here is the article for you: [The mystery of React Element, children, parents and re-renders](https://www.developerway.com/posts/react-elements-children-parents)

**Limitation 3: setting state during render is a no-no**

If you’re trying to catch errors outside of `useEffect` and various callbacks (i.e. during component’s render), then dealing with them properly is not that trivial anymore: state updates during render are not allowed.

Simple code like this, for example, will just cause an infinite loop of re-renders, if an error happens:

```tsx
const Component = () => {
  const [hasError, setHasError] = useState(false);

  try {
    doSomethingComplicated();
  } catch(e) {
    // don't do that! will cause infinite loop in case of an error
    // see codesandbox below with live example
    setHasError(true);
  }
}
```

[Check it out in codesandbox](https://codesandbox.io/s/vibrant-wilbur-02c4fc?file=/src/App.tsx)

We could, of course, just return the error screen here instead of setting state:

```tsx
const Component = () => {
  try {
    doSomethingComplicated();
  } catch(e) {
    // this allowed
    return <SomeErrorScreen />
  }
}
```

But that, as you can imagine, is a bit cumbersome, and will force us to handle errors in the same component differently: state for `useEffect` and callbacks, and direct return for everything else.

```tsx
// while it will work, it's super cumbersome and hard to maitain, don't do that
const SomeComponent = () => {
  const [hasError, setHasError] = useState(false);

  useEffect(() => {
    try {
      // do something like fetching some data
    } catch(e) {
      // can't just return in case of errors in useEffect or callbacks
      // so have to use state
      setHasError(true);
    }
  })

  try {
    // do something during render
  } catch(e) {
    // but here we can't use state, so have to return directly in case of an error
    return <SomeErrorScreen />;
  }

  // and still have to return in case of error state here
  if (hasError) return <SomeErrorScreen />

  return <SomeComponentContent {...datasomething} />
}
```

To summarise this section: if we rely solely on `try/catch` in React, we will either miss most of the errors, or will turn every component into an incomprehensible mess of code that will probably cause errors by itself.

Luckily, there is another way.

## React ErrorBoundary component

To mitigate the limitations from above, React gives us what is known as [“Error Boundaries”](https://reactjs.org/docs/error-boundaries.html): a special API that turns a regular component into a `try/catch` statement in a way, only for React declarative code. Typical usage that you can see in every example over there, including React docs, will be something like this:

```tsx
const Component = () => {
  return (
    <ErrorBoundary>
      <SomeChildComponent />
      <AnotherChildComponent />
    </ErrorBoundary>
  )
}
```

Now, if something goes wrong in any of those components or their children during render, the error will be caught and dealt with.

But React doesn’t give us the component _per se_, it just gives us a tool to implement it. The simplest implementation would be something like this:

```tsx
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    // initialize the error state
    this.state = { hasError: false };
  }

  // if an error happened, set the state to true
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  render() {
    // if error happened, return a fallback component
    if (this.state.hasError) {
      return <>Oh no! Epic fail!</>
    }

    return this.props.children;
  }
}
```

We create a regular class component (going old-school here, no hooks for error boundaries available) and implement `getDerivedStateFromError` method - that turns the component into a proper error boundary.

Another important thing to do when dealing with errors is to send the error info somewhere where it can wake up everyone who’s on-call. For this, error boundaries give us `componentDidCatch` method:

```tsx
class ErrorBoundary extends React.Component {
  // everything else stays the same

  componentDidCatch(error, errorInfo) {
    // send error to somewhere here
    log(error, errorInfo);
  }
}
```

After the error boundary is set up, we can do whatever we want with it, same as any other component. We can, for example, make it more re-usable and pass the fallback as a prop:

```tsx
render() {
  // if error happened, return a fallback component
  if (this.state.hasError) {
    return this.props.fallback;
  }

  return this.props.children;
}
```

And use it like this:

```tsx
const Component = () => {
  return (
    <ErrorBoundary fallback={<>Oh no! Do something!</>}>
      <SomeChildComponent />
      <AnotherChildComponent />
    </ErrorBoundary>
  )
}
```

Or anything else that we might need, like resetting state on a button click, differentiating between types of errors, or pushing that error to a context somewhere.

See full example here:

{% embed https://codesandbox.io/embed/simple-error-boundary-component-4ldsun?file=/src/App.tsx?codemirror=1&hidedevtools=0&view=preview %}


There is one caveat in this error-free world though: it doesn’t catch _everything_.

## ErrorBoundary component: limitations

Error boundary catches only errors that happen during React lifecycle. Things that happen outside of it, like resolved promises, async code with setTimeout, various callbacks and event handlers, will just disappear if not dealt with explicitly.

```tsx
const Component = () => {
  useEffect(() => {
    // this one will be caught by ErrorBoundary component
    throw new Error('Destroy everything!');
  }, [])

  const onClick = () => {
    // this error will just disappear into the void
    throw new Error('Hulk smash!');
  }

  useEffect(() => {
    // if this one fails, the error will also disappear
    fetch('/bla')
  }, [])

  return <button onClick={onClick}>click me</button>
}

const ComponentWithBoundary = () => {
  return (
    <ErrorBoundary>
      <Component />
    </ErrorBoundary>
  )
}
```

The common recommendation here is to use regular try/catch for that kind of errors. And at least here we can use state safely (more or less): callbacks of event handlers are exactly the places where we usually set state anyway. So technically, we can just combine two approaches and do something like this:

```tsx
const Component = () => {
  const [hasError, setHasError] = useState(false);

  // most of the errors in this component and in children will be caught by the ErrorBoundary

  const onClick = () => {
    try {
      // this error will be caught by catch
      throw new Error('Hulk smash!');
    } catch(e) {
      setHasError(true);
    }
  }

  if (hasError) return 'something went wrong';

  return <button onClick={onClick}>click me</button>
}

const ComponentWithBoundary = () => {
  return (
    <ErrorBoundary fallback={"Oh no! Something went wrong"}>
      <Component />
    </ErrorBoundary>
  )
}
```

But. We’re back to square one: every component needs to maintain its “error” state and more importantly - make a decision on what to do with it.

We can, of course, instead of dealing with those errors on a component level just propagate them up to the parent that has `ErrorBoundary` via props or `Context`. That way at least we can have a “fallback” component in just one place:

```tsx
const Component = ({ onError }) => {
  const onClick = () => {
    try {
      throw new Error('Hulk smash!');
    } catch(e) {
      // just call a prop instead of maintaining state here
      onError();
    }
  }

  return <button onClick={onClick}>click me</button>
}

const ComponentWithBoundary = () => {
  const [hasError, setHasError] = useState();
  const fallback = "Oh no! Something went wrong";

  if (hasError) return fallback;

  return (
    <ErrorBoundary fallback={fallback}>
      <Component onError={() => setHasError(true)} />
    </ErrorBoundary>
  )
}
```

But it’s so much additional code! We’d have to do it for every child component in the render tree. Not to mention that we’re basically maintaining two error states now: in the parent component, and in `ErrorBoundary` itself. And `ErrorBoundary` already has all the mechanisms in place to propagate the errors up the tree, we’re doing double work here.

Can’t we just catch those errors from async code and event handlers with `ErrorBoundary` instead?

## Catching async errors with ErrorBoundary

Interestingly enough - we can catch them all with ErrorBoundary! Everyone’s favorite [Dan Abramov](https://twitter.com/dan_abramov) shares with us a cool hack to achieve exactly that: [Throwing Error from hook not caught in error boundary  · Issue #14981 · facebook/react.](https://github.com/facebook/react/issues/14981#issuecomment-468460187)

The trick here is to catch those errors first with `try/catch`, then inside `catch` statement trigger normal React re-render, and then re-throw those errors back into the re-render lifecycle. That way `ErrorBoundary` can catch them as any other error. And since state update is the way to trigger re-render, and state set function can actually [accept a updater function](https://beta.reactjs.org/reference/react/useState#updating-state-based-on-the-previous-state) as an argument, the solution is pure magic:

```tsx
const Component = () => {
  // create some random state that we'll use to throw errors
  const [state, setState] = useState();

  const onClick = () => {
    try {
      // something bad happened
    } catch (e) {
      // trigger state update, with updater function as an argument
      setState(() => {
        // re-throw this error within the updater function
        // it will be triggered during state update
        throw e;
      })
    }
  }
}
```

Full example here:

{% embed https://codesandbox.io/embed/simple-async-error-in-error-boundary-r8l22g?file=/src/App.tsx?codemirror=1&hidedevtools=0&view=preview %}


The final step here would be to abstract that hack away, so we don’t have to create random states in every component. We can go creative here, and make a hook that gives us an async errors thrower:

```tsx
const useThrowAsyncError = () => {
  const [state, setState] = useState();

  return (error) => {
    setState(() => throw error)
  }
}
```

And use it like this:

```tsx
const Component = () => {
  const throwAsyncError = useThrowAsyncError();

  useEffect(() => {
    fetch('/bla').then().catch((e) => {
      // throw async error here!
      throwAsyncError(e)
    })
  })
}
```

Or, we can create a wrapper for callbacks like this:

```tsx
const useCallbackWithErrorHandling = (callback) => {
  const [state, setState] = useState();

  return (...args) => {
    try {
      callback(...args);
    } catch(e) {
      setState(() => throw e);
    }
  }
}
```

And use it like this:

```tsx
const Component = () => {
  const onClick = () => {
    // do something dangerous here
  }

  const onClickWithErrorHandler = useCallbackWithErrorHandling(onClick);

  return <button onClick={onClickWithErrorHandler}>click me!</button>
}
```

Or anything else that your heart desires and the app requires. No limits! And no errors will get away anymore.

Full example here:

{% embed https://codesandbox.io/embed/simple-async-errors-utils-for-error-boundary-fzg5zv?file=/src/App.tsx?codemirror=1&hidedevtools=0&view=preview %}

## Can I just use react-error-boundary instead?

For those of you, who hate re-inventing the wheel or just prefer libraries for already solved problems, there is a nice one that implements a flexible ErrorBoundary component and has a few useful utils similar to those described above: [GitHub - bvaughn/react-error-boundary](https://github.com/bvaughn/react-error-boundary)

Whether to use it or not is just a matter of personal preferences, coding style, and unique situations within your components.

--- 

That is all for today, hope from now on if something bad happens in your app, you’ll be able to deal with the situation with ease and elegance.

And remember:

* `try/catch` blocks won't catch errors inside hooks like `useEffect` and inside any children components
* `ErrorBoundary` can catch them, but it won’t catch errors in async code and event handlers
* Nevertheless, you can make `ErrorBoundary` catch those, you just need to catch them with `try/catch` first and then re-throw them back into the React lifecycle

Live long and error-free! ✌🏼

---

Originally published at [https://www.developerway.com](https://www.developerway.com). The website has more articles like this 😉

[Subscribe to the newsletter](https://www.developerway.com), [connect on LinkedIn](https://www.linkedin.com/in/adevnadia/) or [follow on Twitter](https://twitter.com/adevnadia) to get notified as soon as the next article comes out.