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

Propagate errors while resolving lazy() default export #21639

Closed
wants to merge 6 commits into from

Conversation

gaearon
Copy link
Collaborator

@gaearon gaearon commented Jun 7, 2021

I think this might fix #18768. I haven't tested yet. Basically, the problem is that the Promise has resolved but we never record that because the handler that's meant to change the status throws.

My fix is to make the error handler catch that. I could also change the then branch itself but didn't want to touch the common case. I changed it to just catch this part specifically with a try/catch.

@facebook-github-bot facebook-github-bot added CLA Signed React Core Team Opened by a member of the React Core Team labels Jun 7, 2021
@sizebot
Copy link

sizebot commented Jun 7, 2021

Comparing: 0eea577...b98e967

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.min.js = 127.29 kB 127.29 kB = 40.81 kB 40.81 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js = 130.11 kB 130.11 kB = 41.75 kB 41.75 kB
facebook-www/ReactDOM-prod.classic.js = 404.72 kB 404.72 kB = 74.90 kB 74.90 kB
facebook-www/ReactDOM-prod.modern.js = 393.27 kB 393.27 kB = 73.09 kB 73.09 kB
facebook-www/ReactDOMForked-prod.classic.js = 404.72 kB 404.72 kB = 74.90 kB 74.90 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
oss-experimental/react/cjs/react-unstable-shared-subset.development.js +1.66% 68.89 kB 70.04 kB +1.24% 18.66 kB 18.89 kB
oss-stable-semver/react/cjs/react.development.js +1.54% 74.50 kB 75.64 kB +0.95% 20.02 kB 20.21 kB
oss-stable/react/cjs/react.development.js +1.54% 74.50 kB 75.64 kB +0.95% 20.02 kB 20.21 kB
oss-experimental/react/cjs/react.development.js +1.53% 74.97 kB 76.11 kB +0.98% 20.04 kB 20.24 kB
facebook-react-native/react/cjs/React-dev.js +1.33% 89.86 kB 91.06 kB +0.87% 21.57 kB 21.75 kB
oss-stable-semver/react/umd/react.development.js +1.23% 97.25 kB 98.45 kB +0.72% 25.02 kB 25.20 kB
oss-stable/react/umd/react.development.js +1.23% 97.25 kB 98.45 kB +0.72% 25.02 kB 25.20 kB
oss-experimental/react/umd/react.development.js +1.23% 97.74 kB 98.94 kB +0.74% 25.07 kB 25.25 kB
facebook-www/React-dev.modern.js +1.22% 98.42 kB 99.61 kB +0.83% 23.97 kB 24.17 kB
facebook-www/React-dev.classic.js +1.20% 99.44 kB 100.63 kB +0.83% 24.20 kB 24.40 kB
oss-experimental/react/cjs/react-unstable-shared-subset.production.min.js +0.86% 6.53 kB 6.58 kB +1.16% 2.68 kB 2.71 kB
oss-stable-semver/react/cjs/react.production.min.js +0.80% 7.04 kB 7.10 kB +1.07% 2.81 kB 2.84 kB
oss-stable/react/cjs/react.production.min.js +0.80% 7.04 kB 7.10 kB +1.07% 2.81 kB 2.84 kB
facebook-react-native/react/cjs/React-prod.js +0.77% 16.66 kB 16.78 kB +1.45% 4.28 kB 4.34 kB
facebook-react-native/react/cjs/React-profiling.js +0.77% 16.66 kB 16.78 kB +1.45% 4.28 kB 4.34 kB
facebook-www/React-prod.modern.js +0.76% 16.82 kB 16.94 kB +1.42% 4.35 kB 4.42 kB
facebook-www/React-profiling.modern.js +0.76% 16.82 kB 16.94 kB +1.42% 4.35 kB 4.42 kB
facebook-www/React-prod.classic.js +0.75% 16.96 kB 17.09 kB +1.41% 4.40 kB 4.46 kB
facebook-www/React-profiling.classic.js +0.75% 16.96 kB 17.09 kB +1.41% 4.40 kB 4.46 kB
oss-experimental/react/cjs/react.production.min.js +0.74% 7.56 kB 7.62 kB +1.05% 2.94 kB 2.97 kB
oss-stable-semver/react/umd/react.profiling.min.js +0.51% 10.93 kB 10.98 kB +0.70% 4.41 kB 4.44 kB
oss-stable/react/umd/react.profiling.min.js +0.51% 10.93 kB 10.98 kB +0.70% 4.41 kB 4.44 kB
oss-stable-semver/react/umd/react.production.min.js +0.51% 10.93 kB 10.98 kB +0.73% 4.41 kB 4.44 kB
oss-stable/react/umd/react.production.min.js +0.51% 10.93 kB 10.98 kB +0.73% 4.41 kB 4.44 kB
oss-experimental/react/umd/react.profiling.min.js +0.47% 11.39 kB 11.44 kB +0.88% 4.54 kB 4.58 kB
oss-experimental/react/umd/react.production.min.js +0.47% 11.39 kB 11.44 kB +0.88% 4.54 kB 4.58 kB

Generated by 🚫 dangerJS against b98e967

@gaearon
Copy link
Collaborator Author

gaearon commented Jun 7, 2021

Hmm. Types say we care about using a minimal then(a, b) contract.

@gaearon gaearon requested review from sebmarkbage and acdlite June 7, 2021 15:11
@gaearon
Copy link
Collaborator Author

gaearon commented Jun 7, 2021

OK so there's actually two separate scenarios that could happen:

  • The initializer itself returns undefined
  • The initializer itself returns a Promise resolving to undefined

Ideally both would show good messages.

},
);
if (__DEV__) {
if (defaultExport === undefined) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't this also a Rejected case?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean? It gets Rejected later on throw the catch. This particular check is just to give a nicer message but there could be more reasons .default access throws (e.g. a getter that throws) so I wanted to separate out the DEV message for one particular case and the production check.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, to clarify, if moduleObject.default doesn't throw, but returns undefined for some other reason (no default export) then we'd consider it Resolved. Is that "supported"?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's "supported" in the sense that it's going to throw in reconciler later when we try to render it. The reconciler knows what it can or cannot render. So we don't need to exhaustively check it here.

Whereas if it's loading itself that fails, we don't even get to the reconciler. Because we end up caching undefined instead of a Promise, and keep throw undefined.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess...but it feels weird to not handle it here, where we know the cause.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to avoid adding duplicate production checks in a hot path since we're going to check the same thing later. So the only places I'm adding checks to are the ones we would not check later because they're be incorrectly flagged as Pending. This is more about fixing the state machine of Lazy than about fixing what can or cannot get rendered.

@gaearon
Copy link
Collaborator Author

gaearon commented Jun 7, 2021

So I reshuffled the code a bit to also catch errors caused by thenable not being a thenable. I'm not super happy with the structure but happy to take suggestions if maybe there's an easier way to do it.

The main thing I want to solve is that we should never have a Pending state with undefined as a value while thinking it's a Promise. That's why we get throw undefined.

}
try {
defaultExport = moduleObject.default;
} catch (e) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for a lazy(async () => { import(...) }) case.

);
try {
thenable.then(onFulfill, onReject);
} catch (e) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for a lazy(() => { import(...) }) case.

@sebmarkbage
Copy link
Collaborator

sebmarkbage commented Jun 7, 2021

We should move the undefined check to the reconciler because this is now valid:

const lazyThatResolvesToUndefined = React.lazy(() => importSomeReactNode());

<div>{lazyThatResolvesToUndefined}</div>

It doesn't currently affect Flight because it uses its own resolver but it's valid in the API to do this in user space.

Edit: Correction. Only the check for undefined when the promise resolves to undefined. Not returning a Promise at all is probably not best practice regardless. However it many places where Promises are used you can use either the value or a Promise and this would be one where it's not which is awkward for types. Which is why we won't do this at runtime anymore for return values in render.

Edit 2: I guess this is not valid unless it's wrapped in a default export regardless so nevermind.

Edit 3: Actually it is an issue for the warning.

@sebmarkbage
Copy link
Collaborator

I posted another take: #21642

@gaearon gaearon closed this Jun 7, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed React Core Team Opened by a member of the React Core Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Bug: React.lazy throws undefined instead of an Error object
5 participants