reactjs / react.dev

The React documentation website
https://react.dev/
Creative Commons Attribution 4.0 International
11.03k stars 7.53k forks source link

Suspending to opt-out of SSR keeps logging uncaught errors #6106

Closed iamkd closed 1 year ago

iamkd commented 1 year ago

I've been trying to build a client-only component as described in this section: https://react.dev/reference/react/Suspense#providing-a-fallback-for-server-errors-and-server-only-content

I am using Remix with streaming APIs and it works as expected, but my custom thrown error is getting logged in the console as "Uncaught error: ...", even though it's caught by Suspense. While this does not affect the behavior, it is getting into our monitoring tools and basically looks like something that shouldn't happen.

I'm creating an issue here just to make sure I am not wrong and the docs are not wrong before considering it a bug in Remix or React. Thanks!

iamkd commented 1 year ago

After a bit of digging into React's source code it looks like all the recoverable errors are eventually logged: https://github.com/facebook/react/blob/d1c8cdae3b20a670ee91b684e8e0ad0c400ae51c/packages/react-reconciler/src/ReactFiberWorkLoop.js#L2897-L2909

Still not sure how to handle this though. I'd love to not have console errors for this specific case.

AhmedBaset commented 1 year ago

Hi @iamkd

but my custom thrown error is getting logged in the console as "Uncaught error: ...", even though it's caught by Suspense.

It seems that you are expecting the <Suspense> component to handle and suppress errors. However, <Suspense> is primarily used for handling asynchronous code and rendering a fallback UI while waiting for data to load. It does not have built-in error handling capabilities.

<Suspense> lets you display a fallback until its children have finished loading.

As the documentation mentions when an error occurs within a component, React will propagate the error to the nearest Error Boundary component.

If a component throws an error on the server, React will not abort the server render. Instead, it will find the closest component above it and include its fallback (such as a spinner) into the generated server HTML. The user will see a spinner at first. On the client, React will attempt to render the same component again. If it errors on the client too, React will throw the error and display the closest error boundary (if there).

Error Boundary

Error boundaries are special class components that define static methods static getDerivedStateFromError(error) {} and componentDidCatch(error, errorInfo) {} to handle errors.

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // Update state to indicate that an error has occurred
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // Log or handle the error here
    console.error(error);
    // You can also send the error to monitoring tools if needed
    // handleMonitoring(error, errorInfo.componentStack);
   // TODO
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return this.props.fallback;
    }

    return this.props.children;
  }
}

Then you can wrap a part of your component tree with it:

<ErrorBoundary fallback={<p>Something went wrong</p>}>
  <Profile />
</ErrorBoundary>

The legacy documentation says::

Error boundaries work like a JavaScript catch {} block, but for components. Only class components can be error boundaries. In practice, most of the time you’ll want to declare an error boundary component once and use it throughout your application.

Note that error boundaries only catch errors in the components below them in the tree. An error boundary can’t catch an error within itself. If an error boundary fails trying to render the error message, the error will propagate to the closest error boundary above it. This, too, is similar to how the catch {} block works in JavaScript.

Hopefully this makes sense

iamkd commented 1 year ago

Hi @A7med3bdulBaset! This absolutely makes sense, however the specific section I was referring to is saying:

On the client, React will attempt to render the same component again. If it errors on the client too, React will throw the error and display the closest error boundary. However, if it does not error on the client, React will not display the error to the user since the content was eventually displayed successfully.

Which means that in the case of an example component:

function Chat() {
  if (typeof window === "undefined") {
    throw Error("Skip rendering on server please");
  }

  return <div>chat</div>
}

and it being used like

<Suspense fallback={<div>Placeholder</div>}><Chat /></Suspense>

will return a placeholder in SSR'd HTML and then successfully render the chat div. It will never reach any ErrorBoundary because the client-side render never fails (in this example). I have tested it and error boundary is not triggered. The problem is that in case of successful client-side recovery the error is still logged while being described as a valid way to do things in docs.

rickhanlonii commented 1 year ago

When the docs say "React will not display the error to the user" it means the error boundary / UI. For console reporting, when React recovers from an error, and no onRecoverableError handler is defined for hydrateRoot, we will log the error to the console as an uncaught error.

If you want to silence these errors from the console in Remix, you can provide a onRecoverableError to the hydrateRoot call in entry.client.js like this:


  hydrateRoot(
    document,
    <StrictMode>
      <RemixBrowser />
    </StrictMode>,
    {
      onRecoverableError: (error) => {
        // Ignore known errors.
        if (error.message === 'Skip rendering on server please') {
          return;
        }

        // Passthrough unknown errors, or report to your error logging service.
        console.error('React recovered from an error:', error);
      }
    }
  );