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

Construct Error at invariant call site for clearer stack traces #15877

Merged
merged 1 commit into from
Jun 18, 2019
Merged

Construct Error at invariant call site for clearer stack traces #15877

merged 1 commit into from
Jun 18, 2019

Conversation

motiz88
Copy link
Contributor

@motiz88 motiz88 commented Jun 13, 2019

Summary

Currently, when a React invariant fails, it throws an Error object constructed inside the ReactError (or ReactErrorProd) function. Because the stack trace is captured at the point of Error's construction, it always begins with the same frame for ReactError, no matter specific invariant actually failed.

This PR moves the Error constructor call to the location of the invariant, removing the stack frame inside ReactError.

Test plan

Added a test in ReactError-test.internal.js.


NOTE: The PR summary has been significantly edited a couple of times; see edit history for more context.

@sizebot
Copy link

sizebot commented Jun 13, 2019

ReactDOM: size: 🔺+0.5%, gzip: 🔺+0.3%

Details of bundled changes.

Comparing: cd98e63...765e068

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.profiling.min.js +0.6% +0.2% 109.44 KB 110.14 KB 34.78 KB 34.84 KB NODE_PROFILING
react-dom-unstable-native-dependencies.development.js 0.0% -0.0% 60.76 KB 60.76 KB 15.85 KB 15.85 KB UMD_DEV
ReactDOM-dev.js +0.4% +0.1% 889.04 KB 892.44 KB 197.96 KB 198.24 KB FB_WWW_DEV
react-dom-unstable-native-dependencies.production.min.js 🔺+0.5% 🔺+0.3% 10.68 KB 10.74 KB 3.67 KB 3.68 KB UMD_PROD
ReactDOMServer-dev.js +0.5% +0.2% 135.46 KB 136.11 KB 34.8 KB 34.86 KB FB_WWW_DEV
react-dom-unstable-fire.development.js +0.1% 0.0% 869.29 KB 870.04 KB 198.09 KB 198.16 KB UMD_DEV
react-dom-unstable-fire.production.min.js 🔺+0.6% 🔺+0.1% 106.03 KB 106.72 KB 34.49 KB 34.54 KB UMD_PROD
ReactTestUtils-dev.js +0.6% +0.2% 53.28 KB 53.61 KB 14.32 KB 14.35 KB FB_WWW_DEV
react-dom-unstable-fire.profiling.min.js +0.6% +0.1% 109.26 KB 109.95 KB 35.52 KB 35.55 KB UMD_PROFILING
react-dom-unstable-fire.development.js +0.1% 0.0% 863.65 KB 864.4 KB 196.52 KB 196.58 KB NODE_DEV
react-dom-server.node.development.js +0.1% 0.0% 135.27 KB 135.37 KB 35.81 KB 35.81 KB NODE_DEV
react-dom-unstable-fire.production.min.js 🔺+0.6% 🔺+0.3% 106.03 KB 106.71 KB 33.89 KB 33.98 KB NODE_PROD
react-dom-server.node.production.min.js 🔺+0.8% 🔺+0.4% 19.98 KB 20.14 KB 7.53 KB 7.55 KB NODE_PROD
react-dom.development.js +0.1% 0.0% 868.95 KB 869.7 KB 197.95 KB 198.02 KB UMD_DEV
react-dom-unstable-fire.profiling.min.js +0.6% +0.2% 109.46 KB 110.15 KB 34.79 KB 34.85 KB NODE_PROFILING
react-dom-server.browser.development.js +0.1% 0.0% 137.2 KB 137.31 KB 36.2 KB 36.21 KB UMD_DEV
react-dom.production.min.js 🔺+0.6% 🔺+0.1% 106.01 KB 106.7 KB 34.48 KB 34.52 KB UMD_PROD
ReactFire-dev.js +0.4% +0.1% 888.25 KB 891.65 KB 197.89 KB 198.17 KB FB_WWW_DEV
react-dom-server.browser.production.min.js 🔺+0.9% 🔺+0.3% 19.2 KB 19.36 KB 7.23 KB 7.25 KB UMD_PROD
react-dom.profiling.min.js +0.6% +0.1% 109.24 KB 109.94 KB 35.51 KB 35.54 KB UMD_PROFILING
ReactFire-prod.js 🔺+0.2% 🔺+0.1% 346.18 KB 347.02 KB 63.65 KB 63.74 KB FB_WWW_PROD
react-dom.development.js +0.1% 0.0% 863.3 KB 864.06 KB 196.38 KB 196.44 KB NODE_DEV
ReactFire-profiling.js +0.2% +0.1% 351.89 KB 352.7 KB 64.66 KB 64.73 KB FB_WWW_PROFILING
react-dom-server.browser.development.js +0.1% 0.0% 133.33 KB 133.44 KB 35.27 KB 35.27 KB NODE_DEV
react-dom.production.min.js 🔺+0.6% 🔺+0.3% 106.01 KB 106.7 KB 33.88 KB 33.97 KB NODE_PROD
react-dom-server.browser.production.min.js 🔺+0.9% 🔺+0.4% 19.12 KB 19.29 KB 7.22 KB 7.25 KB NODE_PROD
ReactDOM-prod.js 🔺+0.2% 🔺+0.1% 358.2 KB 359.04 KB 66.11 KB 66.19 KB FB_WWW_PROD
ReactDOMServer-prod.js 🔺+0.4% 🔺+0.3% 47.93 KB 48.14 KB 11.03 KB 11.07 KB FB_WWW_PROD
ReactDOM-profiling.js +0.2% +0.1% 365.17 KB 365.98 KB 67.35 KB 67.43 KB FB_WWW_PROFILING
react-dom-unstable-native-dependencies.development.js 0.0% -0.0% 60.43 KB 60.43 KB 15.73 KB 15.72 KB NODE_DEV
react-dom-unstable-fizz.node.development.js 0.0% -0.1% 3.88 KB 3.88 KB 1.51 KB 1.51 KB NODE_DEV
react-dom-unstable-native-dependencies.production.min.js 🔺+0.6% 🔺+0.3% 10.42 KB 10.48 KB 3.57 KB 3.58 KB NODE_PROD
react-dom-test-utils.development.js +0.1% -0.0% 57.56 KB 57.6 KB 15.82 KB 15.81 KB UMD_DEV
react-dom-unstable-fizz.browser.development.js 0.0% -0.1% 3.81 KB 3.81 KB 1.54 KB 1.54 KB UMD_DEV
react-dom-test-utils.production.min.js 🔺+1.0% 🔺+0.5% 10.81 KB 10.91 KB 3.98 KB 4 KB UMD_PROD
ReactDOMUnstableNativeDependencies-dev.js +0.2% +0.1% 58.9 KB 59.03 KB 14.9 KB 14.92 KB FB_WWW_DEV
ReactDOMUnstableNativeDependencies-prod.js 🔺+0.3% 🔺+0.4% 26.14 KB 26.22 KB 5.26 KB 5.28 KB FB_WWW_PROD
react-dom-test-utils.development.js +0.1% -0.0% 55.9 KB 55.94 KB 15.5 KB 15.5 KB NODE_DEV
react-dom-unstable-fizz.browser.development.js 0.0% +0.1% 3.64 KB 3.64 KB 1.49 KB 1.49 KB NODE_DEV
react-dom-test-utils.production.min.js 🔺+1.0% 🔺+0.6% 10.56 KB 10.67 KB 3.92 KB 3.94 KB NODE_PROD

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactART-dev.js +0.4% +0.2% 540.56 KB 542.88 KB 113.17 KB 113.38 KB FB_WWW_DEV
react-art.development.js +0.1% 0.0% 599.2 KB 599.71 KB 131.4 KB 131.42 KB UMD_DEV
react-art.production.min.js 🔺+0.5% 🔺+0.1% 97.15 KB 97.6 KB 29.92 KB 29.94 KB UMD_PROD
react-art.development.js +0.1% 0.0% 530.11 KB 530.61 KB 113.92 KB 113.94 KB NODE_DEV
react-art.production.min.js 🔺+0.7% 🔺+0.3% 62.13 KB 62.58 KB 19.2 KB 19.27 KB NODE_PROD
ReactART-prod.js 🔺+0.3% 🔺+0.1% 202.68 KB 203.23 KB 34.51 KB 34.55 KB FB_WWW_PROD

react-test-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-test-renderer.development.js +0.1% 0.0% 543.79 KB 544.29 KB 116.79 KB 116.81 KB UMD_DEV
react-test-renderer.production.min.js 🔺+0.7% 🔺+0.3% 63.46 KB 63.92 KB 19.64 KB 19.69 KB UMD_PROD
ReactShallowRenderer-dev.js +0.6% +0.2% 34.8 KB 35.02 KB 8.74 KB 8.76 KB FB_WWW_DEV
react-test-renderer.development.js +0.1% 0.0% 539.33 KB 539.83 KB 115.67 KB 115.7 KB NODE_DEV
react-test-renderer.production.min.js 🔺+0.7% 🔺+0.3% 63.15 KB 63.61 KB 19.43 KB 19.48 KB NODE_PROD
ReactTestRenderer-dev.js +0.4% +0.2% 551.88 KB 554.28 KB 115.7 KB 115.92 KB FB_WWW_DEV
react-test-renderer-shallow.development.js 0.0% -0.0% 41.93 KB 41.93 KB 10.86 KB 10.86 KB UMD_DEV
react-test-renderer-shallow.production.min.js 🔺+0.5% 🔺+0.2% 11.6 KB 11.66 KB 3.55 KB 3.56 KB UMD_PROD
react-test-renderer-shallow.development.js 0.0% -0.1% 36.07 KB 36.07 KB 9.49 KB 9.49 KB NODE_DEV
react-test-renderer-shallow.production.min.js 🔺+0.5% 🔺+0.4% 11.74 KB 11.8 KB 3.67 KB 3.68 KB NODE_PROD

react-native-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactFabric-dev.js +0.4% +0.2% 663.67 KB 666.26 KB 141.09 KB 141.34 KB RN_FB_DEV
ReactFabric-prod.js 🔺+0.7% 🔺+0.4% 245.47 KB 247.21 KB 42.53 KB 42.71 KB RN_FB_PROD
ReactNativeRenderer-dev.js +0.4% +0.2% 674.8 KB 677.41 KB 143.7 KB 143.95 KB RN_OSS_DEV
ReactFabric-profiling.js +0.7% +0.4% 253.91 KB 255.63 KB 44.19 KB 44.36 KB RN_FB_PROFILING
ReactNativeRenderer-prod.js 🔺+0.7% 🔺+0.4% 252.68 KB 254.53 KB 43.73 KB 43.9 KB RN_OSS_PROD
ReactNativeRenderer-profiling.js +0.7% +0.4% 261.09 KB 262.88 KB 45.43 KB 45.59 KB RN_OSS_PROFILING
ReactNativeRenderer-dev.js +0.4% +0.2% 674.9 KB 677.5 KB 143.76 KB 144 KB RN_FB_DEV
ReactNativeRenderer-prod.js 🔺+0.7% 🔺+0.4% 252.68 KB 254.53 KB 43.74 KB 43.91 KB RN_FB_PROD
ReactNativeRenderer-profiling.js +0.7% +0.4% 261.08 KB 262.88 KB 45.43 KB 45.59 KB RN_FB_PROFILING
ReactFabric-dev.js +0.4% +0.2% 663.57 KB 666.15 KB 141.05 KB 141.3 KB RN_OSS_DEV
ReactFabric-prod.js 🔺+0.7% 🔺+0.4% 245.47 KB 247.21 KB 42.51 KB 42.7 KB RN_OSS_PROD
ReactFabric-profiling.js +0.7% +0.4% 253.91 KB 255.63 KB 44.18 KB 44.35 KB RN_OSS_PROFILING

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler-reflection.development.js 0.0% -0.1% 19.14 KB 19.15 KB 6.12 KB 6.12 KB NODE_DEV
react-reconciler-reflection.production.min.js 🔺+2.8% 🔺+1.9% 2.51 KB 2.58 KB 1.11 KB 1.13 KB NODE_PROD
react-reconciler-persistent.development.js +0.1% 0.0% 525.75 KB 526.25 KB 111.37 KB 111.4 KB NODE_DEV
react-reconciler-persistent.production.min.js 🔺+0.7% 🔺+0.1% 63.31 KB 63.78 KB 19.01 KB 19.04 KB NODE_PROD
react-reconciler.development.js +0.1% 0.0% 528.17 KB 528.66 KB 112.4 KB 112.43 KB NODE_DEV
react-reconciler.production.min.js 🔺+0.7% 🔺+0.1% 63.3 KB 63.77 KB 19 KB 19.03 KB NODE_PROD

Generated by 🚫 dangerJS

@acdlite
Copy link
Collaborator

acdlite commented Jun 13, 2019

ReactError and ReactErrorProd are different from invariant in that they are error constructors, not functions that throw.

throw ReactError();

So there's no extra frame that needs popping.

@acdlite
Copy link
Collaborator

acdlite commented Jun 13, 2019

Closing. If I'm mistaken in my reasoning, please let me know with additional details and I'll reopen.

@acdlite acdlite closed this Jun 13, 2019
@motiz88
Copy link
Contributor Author

motiz88 commented Jun 13, 2019

@acdlite, I'm not at my laptop now but I'm quite certain that's incorrect. They are functions that create an Error instance (which is where the stack gets captured) and return it, so there is in fact an extra frame there.

I'm seeing this with the error reporting for FB's RN apps - all the renderer invariants currently get aggregated by that first frame inside ReactError.

@acdlite
Copy link
Collaborator

acdlite commented Jun 13, 2019

They are functions that create an Error instance

Ah right. Ugh... Maybe we should make them actual constructors.

Lemme think about this before merging framesToPop because this affects open source users, too. Would like a solution that works for everyone.

@acdlite acdlite reopened this Jun 13, 2019
@acdlite
Copy link
Collaborator

acdlite commented Jun 13, 2019

What if we changed the error transform so that the error is passed to ReactError as an argument?

throw ReactError(Error(msg));

@motiz88
Copy link
Contributor Author

motiz88 commented Jun 13, 2019

I'd be happy with changing the transform - that sounds like the best way forward for now. A few thoughts:

make them actual constructors

That gets tricky depending on the execution environment. In a native ES2015 environment like Chrome, extending Error works as you'd expect, but under Babel there are some additional frames on the stack. Currently dealing with this internally :)

this affects open source users, too

Just for full context - the framesToPop fix would work for RN users in open source as well. I agree that it's an unnecessary change as far as non-RN users of React are concerned, but given the precedent of invariant I'd bet on it being at least harmless.

Overall, framesToPop isn't a general enough mechanism to standardise or advocate for, so I'm hoping to come up with something better in the next few months. In the case of React, creating the Error where it's thrown will probably suffice.

@gaearon
Copy link
Collaborator

gaearon commented Jun 14, 2019

The bundle size increase is not great. :-(

Can we make this FB-only or something?

@motiz88 motiz88 changed the title Set framesToPop in ReactError and ReactErrorProd Construct Error at invariant call site for clearer stack traces Jun 14, 2019
@motiz88
Copy link
Contributor Author

motiz88 commented Jun 14, 2019

Maybe we can compensate for the increase by making ReactError actually throw. I'll try that first.

@motiz88
Copy link
Contributor Author

motiz88 commented Jun 14, 2019

Hmm, that didn't work the way I wanted it to. I've also tried moving Error() to be its own argument instead of wrapping the message, hoping that would be more compressible, but the improvement wasn't significant.

Options I see:

  1. Roll back to 1488297 (framesToPop approach), which doesn't change the invariant call sites and consequently has a minimal size footprint.
  2. Only modify ReactError (the __DEV__ / noMinify: true path), leaving ReactErrorProd alone. This would satisfy RN's needs since we don't minify errors when building for RN, but ideally I'd want this in the FB_WWW_PROD bundle too.
  3. Pass down a new flag into the invariant transform, only set it for FB bundles, and create ReactError.fb.js and ReactErrorProd.fb.js files that have the new interface. This adds the most complexity but only affects FB.
  4. 💡 Just fork ReactError and ReactErrorProd for FB and go the API-compatible framesToPop route, since all of FB's use cases support this convention.

I think 4 is my favourite option now. Any objections?

EDIT: Doesn't look like there's currently a way to override specific files with .fb.js, just bundle entry points. How open are we to an if (__FB__) { ... } solution? Ideally we'd also amend the testing setup with yarn test-prod-fb and yarn test-fb commands so this code path is covered.

EDIT 2: Found forks.js. Do we have a testing story for forked implementation files?

@motiz88
Copy link
Contributor Author

motiz88 commented Jun 14, 2019

Open questions:

  1. How to test this change? I can think of a few approaches but they're all kinda churny and suboptimal, so I'm going to wait for someone to voice an opinion before proceeding.
  2. Should we enable this fork in RN OSS too? I'm leaning towards yes (since RN's error handler supports framesToPop) but I'd definitely want this to be covered by a good test in that case.

@acdlite
Copy link
Collaborator

acdlite commented Jun 14, 2019

Re: the version where you construct the error at the callsite, I inspected the build output and it looks like it only goes up because Error does not get minified.

- throw z(85)
+ throw z(Error(85))

This doesn't seem like a huge deal to me. Bundle size as a metric is mostly a proxy for parse time, which I wouldn't expect this to contribute to significantly.

However, I wonder what the increase would look like if you alias Error to a variable name?

Maybe we can compensate for the increase by making ReactError actually throw. I'll try that first.

It used to work like that, like invariant, but we changed it to the current version so we can eventually support using normal errors in the source (among other reasons). See #15072

@acdlite
Copy link
Collaborator

acdlite commented Jun 14, 2019

Also I would still like to find a solution that works for people outside Facebook and React Native.

@motiz88
Copy link
Contributor Author

motiz88 commented Jun 14, 2019

If we find the bundle size increase to be acceptable, we have a couple of general solutions ready to go right here in the PR.

Aliasing Error occurred to me, but I'm not sure what a good way to automate that would be. It's also, strictly speaking, a change of behaviour (imagine someone overwriting global.Error after loading React).

I'll implement any changes you folks think are necessary.

@acdlite
Copy link
Collaborator

acdlite commented Jun 14, 2019

imagine someone overwriting global.Error after loading React

Meh, don't care :D

Aliasing Error occurred to me, but I'm not sure what a good way to automate that would be

Import a module that exports Error? :D

I will stamp a version that passes the error in, preferably with Error minified but without it is also fine.

@motiz88
Copy link
Contributor Author

motiz88 commented Jun 18, 2019

@acdlite: Rolled back to 1488297 and then rebased on master as 765e068. I have a local version that aliases Error to a variable (using the module approach & Rollup's neat hoisting) but it doesn't quite seem to pay off in gzip size (https://gist.github.com/motiz88/9a7367876449d91865006b6b861bc533). Let's go with this as-is?

@acdlite acdlite merged commit 9845437 into facebook:master Jun 18, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants