Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
iamkd opened this issue Jun 20, 2023 · 4 comments
Closed

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

iamkd opened this issue Jun 20, 2023 · 4 comments

Comments

@iamkd
Copy link

iamkd commented Jun 20, 2023

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
Copy link
Author

iamkd commented Jun 20, 2023

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
Copy link
Contributor

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
Copy link
Author

iamkd commented Jun 20, 2023

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
Copy link
Member

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);
      }
    }
  );

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants