Always rethrow original error when we replay errors (#20425)

We replay errors so you can break on paused exceptions. This is done in
the second pass so that the first pass can ignore suspense.

Originally this threw the original error. For suppression purposes
we copied the flag onto the original error.

f1dc626b29/packages/react-reconciler/src/ReactFiberScheduler.old.js (L367-L369)

During this refactor it changed to just throw the retried error:

https://github.com/facebook/react/pull/15151

We're not sure exactly why but it was likely just an optimization or
clean up.

So we can go back to throwing the original error. That helps in the case
where a memoized function is naively not rethrowing each time such as
in Node's module system.

Unfortunately this doesn't fix the problem fully.
Because invokeGuardedCallback captures the error and logs it to the browser.
So you still end up seeing the wrong message in the logs.

This just fixes so that the error boundary sees the first one.
This commit is contained in:
Sebastian Markbåge 2020-12-10 08:50:41 -05:00 committed by GitHub
parent b15d6e93e7
commit cdfde3ae11
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 4094 additions and 2710 deletions

View File

@ -241,6 +241,20 @@ class TrySilenceFatalError extends React.Component {
}
}
function naiveMemoize(fn) {
let memoizedEntry;
return function() {
if (!memoizedEntry) {
memoizedEntry = {result: null};
memoizedEntry.result = fn();
}
return memoizedEntry.result;
};
}
let memoizedFunction = naiveMemoize(function() {
throw new Error('Passed');
});
export default class ErrorHandlingTestCases extends React.Component {
render() {
return (
@ -288,6 +302,21 @@ export default class ErrorHandlingTestCases extends React.Component {
}}
/>
</TestCase>
<TestCase title="Throwing memoized result" description="">
<TestCase.Steps>
<li>Click the "Trigger error" button</li>
<li>Click the reset button</li>
</TestCase.Steps>
<TestCase.ExpectedResult>
The "Trigger error" button should be replaced with "Captured an
error: Passed". Clicking reset should reset the test case.
</TestCase.ExpectedResult>
<Example
doThrow={() => {
memoizedFunction().value;
}}
/>
</TestCase>
<TestCase
title="Cross-origin errors (development mode only)"
description="">

File diff suppressed because it is too large Load Diff

View File

@ -3169,14 +3169,22 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
if (hasCaughtError()) {
const replayError = clearCaughtError();
// `invokeGuardedCallback` sometimes sets an expando `_suppressLogging`.
// Rethrow this error instead of the original one.
throw replayError;
} else {
// This branch is reachable if the render phase is impure.
throw originalError;
if (
typeof replayError === 'object' &&
replayError !== null &&
replayError._suppressLogging &&
typeof originalError === 'object' &&
originalError !== null &&
!originalError._suppressLogging
) {
// If suppressed, let the flag carry over to the original error which is the one we'll rethrow.
originalError._suppressLogging = true;
}
}
// We always throw the original error in case the second render pass is not idempotent.
// This can happen if a memoized function or CommonJS module doesn't throw after first invokation.
throw originalError;
}
};
} else {
beginWork = originalBeginWork;

View File

@ -3228,14 +3228,22 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
if (hasCaughtError()) {
const replayError = clearCaughtError();
// `invokeGuardedCallback` sometimes sets an expando `_suppressLogging`.
// Rethrow this error instead of the original one.
throw replayError;
} else {
// This branch is reachable if the render phase is impure.
throw originalError;
if (
typeof replayError === 'object' &&
replayError !== null &&
replayError._suppressLogging &&
typeof originalError === 'object' &&
originalError !== null &&
!originalError._suppressLogging
) {
// If suppressed, let the flag carry over to the original error which is the one we'll rethrow.
originalError._suppressLogging = true;
}
}
// We always throw the original error in case the second render pass is not idempotent.
// This can happen if a memoized function or CommonJS module doesn't throw after first invokation.
throw originalError;
}
};
} else {
beginWork = originalBeginWork;