gremo
gremo

Reputation: 48909

React error boundaries with useEffect and async function, what I'm missing?

In my Hello.jsx component I'm calling an API which could fail. Here a fake API is called loader:

import React, { useEffect } from "react";

export const Hello = () => {
  const loader = async () => {
    return Promise.reject("API error.");
  };

  useEffect(() => {
    const load = async () => {
      await loader();
    };

    load();
  }, []);

  return <h1>Hello World!</h1>;
};

Problem is that the ErrorBoundary component (not displayed here) should print a red message but it doesn't. Error is not "catched". If I throw normally, not inside an async function, it shows the red text "Something went wrong!". Any clue?

<div>
  <ErrorBoundary>
    <Hello />
  </ErrorBoundary>
</div>

Complete CodeSandbox example is here.

Upvotes: 18

Views: 15812

Answers (4)

kta
kta

Reputation: 20110

Besides wrapping up child components in Error boundary, also do the following

import React, { useEffect } from "react";
import { useErrorBoundary  } from 'react-error-boundary';

export const Hello = () => {
  const { showBoundary } = useErrorBoundary();

  const loader = async () => {
    return Promise.reject("API error.");
  };

  useEffect(() => {
    const load = async () => {
      try{
       await loader();
      catch(error){
          showBoundary(error)
      }
    };

    load();
  }, []);

  return <h1>Hello World!</h1>;
};

then it will show in your error boundary fallback ui. Hope this helps who are facing the similar issue. Ref

Upvotes: 1

user2352943
user2352943

Reputation: 21

In your async function catch your exception and throw it inside a react hook callback.

import React, { useEffect, useState } from "react";

export const Hello = () => {
  const [, setError] = useState();
  const loader = async () => {
    return Promise.reject("API error.");
  };

  useEffect(() => {
    const load = async () => {
      try {
        await loader();
      } catch (e) {
        setError(() => {
          // rethrow the error inside a hook this will load the error boundry
          throw e;
        });
      }
    };

    load();
  }, []);

  return <h1>Hello World!</h1>;
};

Upvotes: 1

Matt Carlotta
Matt Carlotta

Reputation: 19762

Problem

Basically, you have two things working against you. The first is that the CRA has an ErrorOverlay which captures errors first and the second is that since event handlers are asynchronous, they don't trigger the componentDidCatch/getDerivedStateFromError lifecycles: Issue #11409.

The work-around is to capture unhandledrejection events on the window.

Solution

Edit React Error Boundary Promises

Code

ErrorBoundary.js

import * as React from "react";

const ErrorBoundary = ({ children }) => {
  const [error, setError] = React.useState("");

  const promiseRejectionHandler = React.useCallback((event) => {
    setError(event.reason);
  }, []);

  const resetError = React.useCallback(() => {
    setError("");
  }, []);

  React.useEffect(() => {
    window.addEventListener("unhandledrejection", promiseRejectionHandler);

    return () => {
      window.removeEventListener("unhandledrejection", promiseRejectionHandler);
    };
    /* eslint-disable react-hooks/exhaustive-deps */
  }, []);

  return error ? (
    <React.Fragment>
      <h1 style={{ color: "red" }}>{error.toString()}</h1>
      <button type="button" onClick={resetError}>
        Reset
      </button>
    </React.Fragment>
  ) : (
    children
  );
};

export default ErrorBoundary;

Hello.js

import React, { useEffect } from "react";

export const Hello = () => {
  const loader = async () => {
    return Promise.reject("API Error");
  };

  useEffect(() => {
    const load = async () => {
      try {
        await loader();
      } catch (err) {
        throw err;
      }
    };

    load();
  }, []);

  return <h1>Hello World!</h1>;
};

index.js

import React from "react";
import { render } from "react-dom";
import { Hello } from "./Hello";
import ErrorBoundary from "./ErrorBoundary";

const App = () => (
  <ErrorBoundary>
    <Hello />
  </ErrorBoundary>
);

render(<App />, document.getElementById("root"));

Other thoughts

A cleaner approach would be to just display a pop-up/notification about the error instead of overriding the entire UI. A larger and more complex UI means an unnecessarily large UI repaint:

Edit React Error UI Notification

Upvotes: 5

Stephen Jennings
Stephen Jennings

Reputation: 13234

The function passed to useEffect is completing successfully. It defines a function, then successfully calls the function. The effect function returns undefined normally, so there is nothing for the error boundary to catch.

Error boundaries do not catch all possible errors. Specifically, from the documentation on error boundaries:

Error boundaries do not catch errors for:

  • Event handlers (learn more)
  • Asynchronous code (e.g. setTimeout or requestAnimationFrame callbacks)
  • Server side rendering
  • Errors thrown in the error boundary itself (rather than its children)

If you want the error boundary to take action, then one option is to save the error in state and rethrow it from the component body. For example:

function MyComponent() {
  const [error, setError] = useState(null);
  if (error) {
    throw error;
  }

  useEffect(() => {
    // If loading fails, save the error in state so the component throws when it rerenders
    load().catch(err => setError(err));
  }, []);

  return <div>...</div>
}

Upvotes: 23

Related Questions