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

Compile invariant directly to throw expressions #15071

Merged
merged 4 commits into from
Mar 18, 2019

Conversation

acdlite
Copy link
Collaborator

@acdlite acdlite commented Mar 9, 2019

This is the first of two PRs to improve our error extraction process.

Ok I went down a bit of a rabbit hole.

It started with this todo:

// TODO: Use invariant so the message is stripped in prod?
value = new Error(
(getComponentName(sourceFiber.type) || 'A React component') +
' suspended while rendering, but no fallback UI was specified.\n' +
'\n' +
'Add a <Suspense fallback=...> component higher in the tree to ' +
'provide a loading indicator or placeholder to display.' +
getStackByFiberInDevAndProd(sourceFiber),
);

For some context, production builds of React do not include error messages. We strip them out at compile time and replace them with error codes and a link to a URL that displays the full message. Our error messages can be as verbose as we want without increasing code size. Here's an example: https://reactjs.org/docs/error-decoder.html?invariant=109&args%5B%5D=Foo.

The error code script works by finding calls to our invariant module, looking up the message in a map, and replacing it with the corresponding code. Because we use invariant throughout our codebase to assert expected behavior, including for user facing errors, this mostly works really well.

But error in the excerpted code above doesn't use invariant. It's part of React's error handling path; we can't use invariant there because invariant will immediately throw. Instead, we create an error object and pass it along to the next step of the error handling algorithm. That means this error is not being replaced with an error code.

While thinking about the best way to solve this, I collected some other flaws in the way we do error extraction:

  • The invariant module is a Facebook-ism that's not commonly used by other developers. It's a marginal burden for first time contributors to learn how to use it. For example, invariant throws an error when its first argument evaluates to false, but most people are accustomed to checking for the positive case: if (cond) throw Error(msg) as opposed to invariant(!cond, msg). It's also not obvious that you're supposed to use invariant for errors if you've never contributed to React before.
  • invariant was originally meant to assert some property of the program that is always supposed to be true. These types of errors should only surface to the user if there's a bug in the program itself. Over time, both Facebook and React have started using it for user facing errors, too.
  • There's minimal enforcement for using invariant in our codebase. We have our sizebot that reports increases in bundle size, but if a PR already increases bundle size for other reasons, a stray Error could increase it slightly further without being noticed by the reviewer.
  • As described above, invariant can only be used to immediately throw an error. It's not suited for cases where you need to create an error and pass it along.

Proposed solution

Compile invariant directly to throw expressions.

This PR turns invariant into a compile-time only module. It turns code like this:

invariant(condition, 'A %s message that contains %s', adj, noun);

into this:

if (!condition) {
  if (__DEV__) {
    throw ReactError(`A ${adj} message that contains ${noun}`);
  } else {
    throw ReactErrorProd(ERR_CODE, [adj, noun]);
  }
}

The only thing ReactError does is return an error whose name is set to "Invariant Violation" to match the existing behavior.

ReactProdError is a special version used in production that throws a minified error code, with a link to see to expanded form. This replaces the reactProdInvariant module.

The runtime behavior is the same as it is today.

Compile normal Error constructors to ReactError and ReactErrorProd

I'll open this part as a separate PR.

Use a lint rule to enforce that error messages are written in the correct format

This will go in the next PR, too.

@acdlite acdlite changed the title Transform invariant to custom error type Compile invariant directly to throw expressions Mar 9, 2019
@acdlite acdlite requested a review from keyz March 9, 2019 02:37
@sizebot
Copy link

sizebot commented Mar 9, 2019

React: size: -2.6%, gzip: -2.7%

ReactDOM: size: -0.3%, gzip: -0.6%

Details of bundled changes.

Comparing: df7b87d...8e7cb77

react

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react.development.js -0.1% -0.3% 100.47 KB 100.37 KB 26.18 KB 26.1 KB UMD_DEV
react.production.min.js -2.6% -2.7% 12.34 KB 12.02 KB 4.74 KB 4.61 KB UMD_PROD
react.profiling.min.js -2.2% -2.6% 14.49 KB 14.17 KB 5.26 KB 5.13 KB UMD_PROFILING
react.development.js -0.2% -0.5% 63.02 KB 62.92 KB 17.08 KB 16.99 KB NODE_DEV
react.production.min.js -4.8% -4.5% 6.7 KB 6.38 KB 2.77 KB 2.64 KB NODE_PROD
React-dev.js +1.9% +1.6% 60.07 KB 61.19 KB 16 KB 16.27 KB FB_WWW_DEV
React-prod.js -0.8% -1.0% 15.38 KB 15.26 KB 4.11 KB 4.07 KB FB_WWW_PROD
React-profiling.js -0.8% -1.0% 15.38 KB 15.26 KB 4.11 KB 4.07 KB FB_WWW_PROFILING

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.development.js +0.9% +0.4% 781.07 KB 788.24 KB 178.22 KB 178.95 KB UMD_DEV
react-dom.production.min.js -0.3% -0.6% 105.24 KB 104.97 KB 34.12 KB 33.91 KB UMD_PROD
react-dom.profiling.min.js -0.3% -0.6% 108.24 KB 107.95 KB 34.82 KB 34.6 KB UMD_PROFILING
react-dom.development.js +0.9% +0.4% 775.46 KB 782.63 KB 176.7 KB 177.39 KB NODE_DEV
react-dom.production.min.js -0.3% -0.7% 105.22 KB 104.94 KB 33.66 KB 33.43 KB NODE_PROD
react-dom.profiling.min.js -0.3% -0.7% 108.33 KB 108.04 KB 34.27 KB 34.04 KB NODE_PROFILING
ReactDOM-dev.js +0.9% +0.5% 798.3 KB 805.66 KB 177.83 KB 178.69 KB FB_WWW_DEV
ReactDOM-prod.js -1.4% -0.8% 323.9 KB 319.45 KB 59.37 KB 58.88 KB FB_WWW_PROD
ReactDOM-profiling.js -1.5% -0.9% 329.83 KB 324.73 KB 60.7 KB 60.17 KB FB_WWW_PROFILING
react-dom-unstable-fire.development.js +0.9% +0.4% 781.42 KB 788.58 KB 178.37 KB 179.09 KB UMD_DEV
react-dom-unstable-fire.production.min.js -0.3% -0.6% 105.25 KB 104.99 KB 34.13 KB 33.92 KB UMD_PROD
react-dom-unstable-fire.profiling.min.js -0.3% -0.6% 108.26 KB 107.96 KB 34.83 KB 34.61 KB UMD_PROFILING
react-dom-unstable-fire.development.js +0.9% +0.4% 775.8 KB 782.97 KB 176.84 KB 177.52 KB NODE_DEV
react-dom-unstable-fire.production.min.js -0.3% -0.7% 105.23 KB 104.96 KB 33.67 KB 33.44 KB NODE_PROD
react-dom-unstable-fire.profiling.min.js -0.3% -0.7% 108.35 KB 108.06 KB 34.28 KB 34.05 KB NODE_PROFILING
ReactFire-dev.js +0.9% +0.5% 797.51 KB 804.87 KB 177.74 KB 178.58 KB FB_WWW_DEV
ReactFire-prod.js -1.4% -0.9% 312.44 KB 308.04 KB 56.99 KB 56.49 KB FB_WWW_PROD
ReactFire-profiling.js -1.6% -0.8% 318.46 KB 313.37 KB 58.3 KB 57.82 KB FB_WWW_PROFILING
react-dom-test-utils.development.js +0.7% -0.3% 47.11 KB 47.45 KB 13.01 KB 12.97 KB UMD_DEV
react-dom-test-utils.production.min.js -3.1% -3.8% 10.27 KB 9.95 KB 3.8 KB 3.66 KB UMD_PROD
react-dom-test-utils.development.js +0.7% -0.3% 46.83 KB 47.17 KB 12.94 KB 12.9 KB NODE_DEV
react-dom-test-utils.production.min.js -3.2% -4.0% 10.05 KB 9.73 KB 3.74 KB 3.59 KB NODE_PROD
ReactTestUtils-dev.js +3.1% +2.4% 43.79 KB 45.14 KB 11.89 KB 12.17 KB FB_WWW_DEV
react-dom-unstable-native-dependencies.development.js -0.1% -0.7% 60.61 KB 60.53 KB 15.92 KB 15.8 KB UMD_DEV
react-dom-unstable-native-dependencies.production.min.js -2.9% -3.8% 11.01 KB 10.69 KB 3.81 KB 3.67 KB UMD_PROD
react-dom-unstable-native-dependencies.development.js -0.1% -0.7% 60.28 KB 60.2 KB 15.79 KB 15.67 KB NODE_DEV
react-dom-unstable-native-dependencies.production.min.js -3.0% -3.8% 10.75 KB 10.42 KB 3.71 KB 3.57 KB NODE_PROD
ReactDOMUnstableNativeDependencies-dev.js +1.8% +1.6% 57.62 KB 58.68 KB 14.62 KB 14.85 KB FB_WWW_DEV
ReactDOMUnstableNativeDependencies-prod.js -0.5% -0.8% 26.27 KB 26.14 KB 5.3 KB 5.25 KB FB_WWW_PROD
react-dom-server.browser.development.js +1.0% +0.2% 130.49 KB 131.85 KB 34.82 KB 34.89 KB UMD_DEV
react-dom-server.browser.production.min.js -1.3% -2.0% 19.19 KB 18.93 KB 7.28 KB 7.14 KB UMD_PROD
react-dom-server.browser.development.js +1.1% +0.2% 126.62 KB 127.98 KB 33.9 KB 33.97 KB NODE_DEV
react-dom-server.browser.production.min.js -1.3% -2.0% 19.12 KB 18.86 KB 7.28 KB 7.14 KB NODE_PROD
ReactDOMServer-dev.js +1.9% +1.1% 127.59 KB 130 KB 33.28 KB 33.64 KB FB_WWW_DEV
ReactDOMServer-prod.js 🔺+0.1% -0.3% 45.62 KB 45.67 KB 10.6 KB 10.56 KB FB_WWW_PROD
react-dom-server.node.development.js +1.0% +0.2% 128.68 KB 129.92 KB 34.45 KB 34.51 KB NODE_DEV
react-dom-server.node.production.min.js -1.3% -2.0% 19.99 KB 19.72 KB 7.59 KB 7.44 KB NODE_PROD

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-art.development.js +0.9% +0.4% 553.37 KB 558.31 KB 120.46 KB 120.9 KB UMD_DEV
react-art.production.min.js -0.3% -0.8% 97.06 KB 96.78 KB 29.9 KB 29.66 KB UMD_PROD
react-art.development.js +1.0% +0.4% 484.26 KB 489.21 KB 103.14 KB 103.58 KB NODE_DEV
react-art.production.min.js -0.4% -0.8% 61.98 KB 61.76 KB 19.02 KB 18.87 KB NODE_PROD
ReactART-dev.js +1.1% +0.6% 493.22 KB 498.55 KB 102.26 KB 102.85 KB FB_WWW_DEV
ReactART-prod.js -0.6% -0.8% 195.76 KB 194.53 KB 33.3 KB 33.02 KB FB_WWW_PROD

react-native-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactNativeRenderer-dev.js -0.1% -0.1% 618.99 KB 618.33 KB 132.53 KB 132.4 KB RN_FB_DEV
ReactNativeRenderer-prod.js -0.2% -0.4% 246.03 KB 245.59 KB 43.08 KB 42.92 KB RN_FB_PROD
ReactNativeRenderer-profiling.js -0.2% -0.4% 252.25 KB 251.82 KB 44.43 KB 44.27 KB RN_FB_PROFILING
ReactNativeRenderer-dev.js -0.1% -0.1% 618.9 KB 618.24 KB 132.5 KB 132.36 KB RN_OSS_DEV
ReactNativeRenderer-prod.js -0.2% -0.4% 246.04 KB 245.6 KB 43.08 KB 42.91 KB RN_OSS_PROD
ReactNativeRenderer-profiling.js -0.2% -0.4% 252.27 KB 251.84 KB 44.43 KB 44.26 KB RN_OSS_PROFILING
ReactFabric-dev.js -0.1% -0.1% 609.42 KB 608.75 KB 130.22 KB 130.08 KB RN_FB_DEV
ReactFabric-prod.js -0.2% -0.4% 240.1 KB 239.66 KB 41.88 KB 41.72 KB RN_FB_PROD
ReactFabric-profiling.js -0.2% -0.4% 245.42 KB 244.98 KB 43.24 KB 43.08 KB RN_FB_PROFILING
ReactFabric-dev.js -0.1% -0.1% 609.32 KB 608.66 KB 130.17 KB 130.03 KB RN_OSS_DEV
ReactFabric-prod.js -0.2% -0.4% 240.1 KB 239.67 KB 41.87 KB 41.71 KB RN_OSS_PROD
ReactFabric-profiling.js -0.2% -0.4% 245.43 KB 244.99 KB 43.24 KB 43.07 KB RN_OSS_PROFILING

react-test-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-test-renderer.development.js +1.0% +0.4% 493.09 KB 498.13 KB 104.74 KB 105.17 KB UMD_DEV
react-test-renderer.production.min.js -0.4% -1.2% 62.99 KB 62.71 KB 19.32 KB 19.09 KB UMD_PROD
react-test-renderer.development.js +1.0% +0.4% 488.61 KB 493.65 KB 103.59 KB 104.02 KB NODE_DEV
react-test-renderer.production.min.js -0.4% -0.9% 62.64 KB 62.39 KB 19.09 KB 18.93 KB NODE_PROD
ReactTestRenderer-dev.js +1.1% +0.6% 498.52 KB 503.95 KB 103.18 KB 103.8 KB FB_WWW_DEV
react-test-renderer-shallow.development.js +0.3% -0.9% 38.82 KB 38.95 KB 9.89 KB 9.8 KB UMD_DEV
react-test-renderer-shallow.production.min.js -2.7% -4.0% 11.73 KB 11.42 KB 3.65 KB 3.51 KB UMD_PROD
react-test-renderer-shallow.development.js +0.4% -1.0% 33.05 KB 33.18 KB 8.49 KB 8.4 KB NODE_DEV
react-test-renderer-shallow.production.min.js -2.6% -3.6% 11.92 KB 11.61 KB 3.77 KB 3.63 KB NODE_PROD
ReactShallowRenderer-dev.js +4.0% +3.5% 31.7 KB 32.95 KB 7.95 KB 8.23 KB FB_WWW_DEV

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler.development.js +1.0% +0.4% 482.69 KB 487.69 KB 101.73 KB 102.17 KB NODE_DEV
react-reconciler.production.min.js -0.4% -1.0% 63.16 KB 62.89 KB 18.93 KB 18.73 KB NODE_PROD
react-reconciler-persistent.development.js +1.0% +0.4% 480.68 KB 485.67 KB 100.93 KB 101.36 KB NODE_DEV
react-reconciler-persistent.production.min.js -0.4% -1.0% 63.17 KB 62.91 KB 18.93 KB 18.73 KB NODE_PROD
react-reconciler-reflection.development.js 0.0% -1.5% 15.76 KB 15.76 KB 4.98 KB 4.9 KB NODE_DEV
react-reconciler-reflection.production.min.js -12.2% -12.9% 2.7 KB 2.37 KB 1.23 KB 1.07 KB NODE_PROD

create-subscription

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
create-subscription.development.js -3.7% -4.2% 8.34 KB 8.03 KB 2.9 KB 2.77 KB NODE_DEV
create-subscription.production.min.js -11.4% -10.6% 2.83 KB 2.5 KB 1.32 KB 1.18 KB NODE_PROD

jest-react

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
jest-react.development.js -4.8% -7.1% 7.25 KB 6.9 KB 2.67 KB 2.48 KB NODE_DEV
jest-react.production.min.js -11.3% -9.8% 2.87 KB 2.54 KB 1.42 KB 1.28 KB NODE_PROD
JestReact-dev.js +19.2% +18.1% 4.09 KB 4.87 KB 1.44 KB 1.7 KB FB_WWW_DEV
JestReact-prod.js -2.8% -2.1% 3.51 KB 3.41 KB 1.28 KB 1.26 KB FB_WWW_PROD

Generated by 🚫 dangerJS

@sebmarkbage
Copy link
Collaborator

AFAIK, we still need to compile to invariant(...) calls for the FB builds because we have a different transform internally that deals with those. Has that changed?

@acdlite
Copy link
Collaborator Author

acdlite commented Mar 9, 2019

Not sure, I was hoping one of you had more context on that.

@acdlite
Copy link
Collaborator Author

acdlite commented Mar 9, 2019

Looks like Logview will still get the messages but our invariant module does other stuff I'm less sure about so I'll compile throw ReactProdError to invariant in the FB builds.

@sebmarkbage
Copy link
Collaborator

I'll yield to @gaearon (www) and @sahrens (fbsource) to review this.

Copy link
Collaborator

@gaearon gaearon left a comment

Choose a reason for hiding this comment

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

Haven't looked at www side yet, will check tomorrow.


function ReactError(message) {
const error = new Error(message);
error.name = 'Invariant Violation';
Copy link
Collaborator

Choose a reason for hiding this comment

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

While we're at it, can we just say "React Error"? Is that a breaking change? 😄

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'd be comfortable changing in a minor

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it's a breaking change. It breaks continuity in logs.

@acdlite
Copy link
Collaborator Author

acdlite commented Mar 11, 2019

I looked into it more, and in www, the only things the invariant module does is:

  • Strip out the error message in production. We specifically don't want this behavior because it removes our error codes. @gaearon previously built a way to opt us out of this transform but compiling directly to throw statements sidesteps this problem.
  • Remove the invariant call from the stack trace. Again, by compiling directly to throw statements instead of adding another indirection, we avoid this problem completely.

I'll look into fbsource, too.

@acdlite
Copy link
Collaborator Author

acdlite commented Mar 11, 2019

AFAICT the version in fbsource does nothing special at all.

@acdlite acdlite force-pushed the reacterror branch 2 times, most recently from 49f5282 to 96d30de Compare March 11, 2019 20:40
Copy link
Collaborator

@gaearon gaearon left a comment

Choose a reason for hiding this comment

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

looks good

scripts/error-codes/extract-errors.js Outdated Show resolved Hide resolved
scripts/error-codes/minify-error-messages.js Outdated Show resolved Hide resolved
This transforms calls to the invariant module:

```js
invariant(condition, 'A %s message that contains %s', adj, noun);
```

Into throw statements:

```js
if (!condition) {
  if (__DEV__) {
    throw ReactError(`A ${adj} message that contains ${noun}`);
  } else {
    throw ReactErrorProd(ERR_CODE, [adj, noun]);
  }
}
```

The only thing ReactError does is return an error whose name is set
to "Invariant Violation" to match the existing behavior.

ReactErrorProd is a special version used in production that throws
a minified error code, with a link to see to expanded form. This
replaces the reactProdInvariant module.

As a next step, I would like to replace our use of the invariant module
for user facing errors by transforming normal Error constructors to
ReactError and ReactErrorProd. (We can continue using invariant for
internal React errors that are meant to be unreachable, which was the
original purpose of invariant.)
I wasn't sure about this part so I asked Sebastian, and his rationale
was that using arguments will make ReactErrorProd slightly slower, but
using an array will likely make all the functions that throw slightly
slower to compile, so it's hard to say which way is better. But since
ReactErrorProd is in an error path, and fewer bytes is generally better,
no array is good.
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.

5 participants