react: Triggering suspense with rejected promise causes re-render instead of error boundary

Do you want to request a feature or report a bug?

This might be a bug. @gaearon and @sebmarkbage shared differing opinions on it in this twitter thread

What is the current behavior?

If you throw a promise that rejects from a react component’s render function, that rejection will be completely ignored. It will not show up in browser console, nor will it trigger a React error boundary. Instead, it will trigger a re-render (the same as if the promise had resolved).

Codepen example

What is the expected behavior?

My expectation was that the error boundary would be hit and the component would not re-render. Sebastian’s tweet indicates that that is not the desired behavior, though.

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?

This impacts react@experimental, and also react@>=16.9.0

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Comments: 19 (10 by maintainers)

Most upvoted comments

Suspense has a very specific and strict contract. It’s not currently documented, which is why we’re asking not to build integrations with it except as an experiment. The examples above do not satisfy that contract.

Here’s this contract in a nutshell. A Suspense-compatible cache should follow these rules:

  • It returns the value (or throws an error) synchronously, if it’s fetched for the given key.
  • It throws a Promise if a value is not fetched for the given key yet, and starts the fetch.
    • If data for the requested key is already being fetched, the same exact Promise must be thrown. (So the Promise itself is stored in the cache until it settles.)
    • When the Promise resolves, the cache is updated with either the result (so that it can be read successfully next time), or an error (so that it is rethrown next time).
      • Note that the Promise’s value is ignored by React. What matters is that we put the result into the cache so that React can read it during the retry.
    • The result is then cached. This cache is “append-only”: once we put the fetched data into the cache, we never mutate or remove it. (There is a separate mechanism for invalidation in React itself, which amounts to replacing the cache with an empty one — see https://github.com/reactwg/react-18/discussions/25 for an early look.)

The bolded part explains that you’re supposed to cache the errors, too. We’ll document this contract when Suspense for data fetching is considered stable.

The mechanism just needs a callback really. We kind of abused the Promise object for this purpose but it causes more confusion as a result. That’s why we’ll likely just switch to a different API for this.

The wrapper could definitely accept a promise and rejecting it would cause it to trigger an error boundary.

The point of the implementation details is that React may not store anything about where the promise was used. Including the error boundary. So depending on the details (such as if parent props have changed in the meantime) we may need to reexecute the renders to figure out which error boundary to trigger and what to render instead.

So semantically the real error comes from the function throwing an error after being reexecuted. This should be synchronous and not after one tick which is what would happen with a Promise.

Anyway you should probably file a new issue so we don’t spam this one.