A Guide to Handling Errors in Preact

nikolalsvk

Nikola Đuza

Posted on May 10, 2023

A Guide to Handling Errors in Preact

To err is human. It happens all the time. If we don't account for errors on our websites, then our users will get an awful browsing experience. That's why it is better to plan for errors.

In today's post, we'll go through different ways of handling errors in Preact applications.

Let's get going!

Catching Rendering Errors in Preact

Since Preact is a lightweight React alternative, similar rules to the React world also apply here. You can't use the classic try...catch method of resolving errors while rendering because it's meant to handle imperative code.

But what is a typical rendering error, you're probably asking? Imagine a component like this:

import { h } from "preact";

const CrashableComponent = (props) => {
  return <>{props.iDontExist.meNeither}</>;
};

export default CrashableComponent;
Enter fullscreen mode Exit fullscreen mode

Trying to render CrashableComponent on its own will break the page, leaving the user stranded. We also get an error in the browser console like this one:

Rendering error

So the classic try...catch method won't do us any good here. What will do the job and ensure the page is still usable when an error like this happens? To properly handle rendering errors in Preact, you can use these two approaches:

  1. componentDidCatch
  2. useErrorBoundary hook

We'll go through both in the following sections.

1. Catch Rendering Errors Using componentDidCatch

One way to handle errors is to create a class component that implements the componentDidCatch lifecycle method. It can look something like this:

import { h, Component } from "preact";

class Catcher extends Component {
  constructor() {
    super();
    this.state = { errored: false, error: null };
  }

  componentDidCatch(error) {
    this.setState({ errored: true, error });
  }

  render(props, state) {
    if (state.errored) {
      return (
        <>
          <h1>Oh no! I am a Catcher</h1>
          <p>Listen up, something went badly wrong 😭</p>
          <p>Here's the error message: {state.error.message}</p>
        </>
      );
    }

    return props.children;
  }
}

export default Catcher;
Enter fullscreen mode Exit fullscreen mode

Then, we need to wrap our problematic component (or components) with the Catcher component, like so:

<Catcher>
  <CrashableComponent />
</Catcher>
Enter fullscreen mode Exit fullscreen mode

Now, when we render the page, it will work and show the fallback code we defined in the Catcher component:

Caught error with class component

Error handling using componentDidCatch will work for any error in the child components. Some folks use a global error catcher that wraps most of the application as a root or near-root component.

What is great about this approach is that you can write any logic you want in componentDidCatch. It's a good idea to push the error to an error-tracking service like AppSignal. Here's an example:

componentDidCatch(error) {
  appsignal.sendError(error)
}
Enter fullscreen mode Exit fullscreen mode

Now let's see how we can handle rendering errors in Preact with the useErrorBoundary hook.

2. useErrorBoundary Hook to Catch Rendering Errors

Preact exports a hook you can use to catch and easily retry an error. The hook is called useErrorBoundary, and can go within a component:

const [error, resetError] = useErrorBoundary();
Enter fullscreen mode Exit fullscreen mode

To better illustrate this, let's create a functional component in Preact that handles errors for us:

import { h } from "preact";
import { useErrorBoundary } from "preact/hooks";

const CatcherWithHook = (props) => {
  const [error, resetError] = useErrorBoundary();

  if (error) {
    return (
      <>
        <h1>Oh no! I am a CatcherWithHook</h1>
        <p>Something went badly wrong and useErrorBoundary was used 😭</p>
        <p>Here's the error message: {error.message}</p>

        <button onClick={resetError}>Try again</button>
      </>
    );
  }

  return props.children;
};

export default CatcherWithHook;
Enter fullscreen mode Exit fullscreen mode

We get the CatcherWithHook component, which is leaner than the original Catcher with the componentDidCatch implementation. Also, using the useErrorBoundary is more convenient because it takes care of the resetError part for you. With componentDidCatch, you have to take care of and reset the state manually.

To make sure errors are caught, we need to wrap the problematic code with the new component we've made:

<CatcherWithHook>
  <CrashableComponent />
</CatcherWithHook>
Enter fullscreen mode Exit fullscreen mode

Now, when we render the page, it will show a fallback message and a button to retry the rendering, like so:

Caught error with a functional component and a hook in Preact

The same rule applies here — any rendering error in a child component of CatcherWithHook will be caught, and a fallback UI will be shown.

An important thing to note is that you can leverage an optional callback for monitoring purposes and pass that as the first argument to useErrorBoundary. So if you want to report the error to a service like AppSignal, here's how to do it:

const [error] = useErrorBoundary((error) => appsignal.sendError(error));
Enter fullscreen mode Exit fullscreen mode

Awesome - we covered all of Preact's features for handling errors! But we have to be aware that componentDidCatch and useErrorBoundary won't catch all errors. They do not catch errors for:

  • Event handlers
  • Asynchronous code (e.g., setTimeout or requestAnimationFrame callbacks)
  • Errors that are thrown in the error boundary itself (rather than its children)

You still need to use the try...catch statement for these cases. So let's go ahead and show how you can do that.

Preact Error Catching in Event Handlers

As mentioned before, tools like componentDidCatch and useErrorBoundary can't help us with errors in event handlers. For those scenarios, we should go back to the old-fashioned try...catch block:

import { h } from "preact";
import { useState } from "preact/hooks";

const CrashIfClicked = () => {
  const [error, setError] = useState(null);

  if (error) {
    return (
      <>
        <p>An error happened, sorry!</p>
        <button onClick={() => setError(null)}>Try again</button>
      </>
    );
  }

  return (
    <>
      <p>This is a component that will crash the app.</p>

      <button
        onClick={() => {
          try {
            throw new Error("Crash!");
          } catch (e) {
            setError(e);
          }
        }}
      >
        Crash!
      </button>
    </>
  );
};

export default CrashIfClicked;
Enter fullscreen mode Exit fullscreen mode

If we try to render this crashable button and interact with it, this will happen.

A similar thing happens with errors in setTimeout calls.

Preact Error Catching in setTimeout Calls

Imagine a similar component, but it has a setTimeout call in the event handler. Here it is:

import { h } from "preact";
import { useState } from "preact/hooks";

const CrashAfterAWhile = () => {
  const [error, setError] = useState(null);

  if (error) {
    return (
      <>
        <p>An error happened, sorry!</p>
        <button onClick={() => setError(null)}>Try again</button>
      </>
    );
  }

  return (
    <>
      <p>
        This is a component that will crash after 1 second within clicking the
        button.
      </p>

      <button
        onClick={() => {
          setTimeout(() => {
            try {
              throw new Error("I crashed after 1 second!");
            } catch (e) {
              setError(e);
            }
          }, 1000);
        }}
      >
        Crash after 1 second!
      </button>
    </>
  );
};

export default CrashAfterAWhile;
Enter fullscreen mode Exit fullscreen mode

When we interact with the button, it will throw an error after 1000 milliseconds. Then, we will catch the error and show a fallback message. Here's how it looks.

That's nice and dandy! Our app is catching errors and showing messages and retry buttons to our users instead of blank screens.

There's also one other way to handle errors — by using AppSignal's Preact integration.

Tracking Errors in Preact with AppSignal

AppSignal has a @appsignal/preact package that requires Preact 10.0.0 or higher. You can add it to your project with these commands:

yarn add @appsignal/javascript @appsignal/preact
npm install --save @appsignal/javascript @appsignal/preact
Enter fullscreen mode Exit fullscreen mode

Then in the code, you can use the ErrorBoundary provided by @appsignal/preact like so:

import { h, Component } from "preact";
import Appsignal from "@appsignal/javascript";

import { ErrorBoundary } from "@appsignal/preact";

export const appsignal = new Appsignal({
  key: "YOUR FRONTEND API KEY",
});

const FallbackComponent = ({ error }) => (
  <>
    <p>Uh oh! There was an error caught by AppSignal's ErrorBoundary :(</p>
    <p>Here's the error: {error.message}</p>
  </>
);

class AppSignalCatcher extends Component {
  render(props) {
    return (
      <ErrorBoundary
        instance={appsignal}
        tags={{ tag: "value" }}
        fallback={(error) => <FallbackComponent error={error} />}
      >
        {props.children}
      </ErrorBoundary>
    );
  }
}

export default AppSignalCatcher;
Enter fullscreen mode Exit fullscreen mode

To make sure errors are caught, we need to wrap the child components with the new component we've made:

<AppSignalCatcher>
  <CrashableComponent />
</AppSignalCatcher>
Enter fullscreen mode Exit fullscreen mode

Then we should see the fallback errors message in our app like so:

AppSignal's ErrorBoundary caught an error

Here's an example of how this error looks in AppSignal:

Preact error message

Using ErrorBoundary provided by AppSignal is great because you can:

  • Catch rendering errors
  • Show a fallback UI for users
  • Report any mishaps to AppSignal so they can be tracked

Wrapping Up

Thanks for reading this blog post about handling errors in Preact. Check out this GitHub repo for all the code, with examples.

In summary, Preact features like the componentDidCatch lifecycle method and useReactBoundary hook are great for catching errors in declarative code (e.g., inside their child component tree).

For other cases, you need to use a try...catch statement (e.g., async calls like setTimeout, event handlers, and errors thrown in the error boundary itself).

That's all, folks. Thanks for reading, and catch you in the next one!

P.S. If you liked this post, subscribe to our JavaScript Sorcery list for a monthly deep dive into more magical JavaScript tips and tricks.

💖 💪 🙅 🚩
nikolalsvk
Nikola Đuza

Posted on May 10, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related