+ {this.props.children}
+
+
+
+ Don't forget to uncheck "Silence errors" when you're done with
+ this test!
+
+
+ )}
+
+ );
+ }
+}
+
+class SilenceRecoverableError extends React.Component {
+ render() {
+ return (
+
+ {
+ throw new Error('Silenced error (render phase)');
+ }}
+ />
+ {
+ throw new Error('Silenced error (commit phase)');
+ }}
+ />
+
+ );
+ }
+}
+
+class TrySilenceFatalError extends React.Component {
+ container = document.createElement('div');
+
+ triggerErrorAndCatch = () => {
+ try {
+ ReactDOM.flushSync(() => {
+ ReactDOM.render(
+ {
+ throw new Error('Caught error');
+ }}
+ />,
+ this.container
+ );
+ });
+ } catch (e) {}
+ };
+
+ render() {
+ return (
+
+
+
+ );
+ }
+}
+
export default class ErrorHandlingTestCases extends React.Component {
render() {
return (
@@ -103,6 +222,12 @@ export default class ErrorHandlingTestCases extends React.Component {
the BadRender component. After resuming, the "Trigger error" button
should be replaced with "Captured an error: Oops!" Clicking reset
should reset the test case.
+
+
+ In the console, you should see two messages: the actual error
+ ("Oops") printed natively by the browser with its JavaScript stack,
+ and our addendum ("The above error occurred in BadRender component")
+ with a React component stack.
{
@@ -155,10 +280,44 @@ export default class ErrorHandlingTestCases extends React.Component {
Open the console. "Uncaught Error: Caught error" should have been
- logged by the browser.
+ logged by the browser. You should also see our addendum ("The above
+ error...").
+
+
+
Check the "Silence errors" checkbox below
+
Click the "Throw (render phase)" button
+
Click the "Throw (commit phase)" button
+
Uncheck the "Silence errors" checkbox
+
+
+ Open the console. You shouldn't see any messages in the
+ console: neither the browser error, nor our "The above error"
+ addendum, from either of the buttons. The buttons themselves should
+ get replaced by two labels: "Captured an error: Silenced error
+ (render phase)" and "Captured an error: Silenced error (commit
+ phase)".
+
+
+
+
+
+
Check the "Silence errors" checkbox below
+
Click the "Throw fatal error" button
+
Uncheck the "Silence errors" checkbox
+
+
+ Open the console. "Error: Caught error" should have been logged by
+ React. You should also see our addendum ("The above error...").
+
+
+
);
}
diff --git a/fixtures/dom/src/style.css b/fixtures/dom/src/style.css
index c316bb3f37e..a8e1391e1d8 100644
--- a/fixtures/dom/src/style.css
+++ b/fixtures/dom/src/style.css
@@ -158,7 +158,7 @@ li {
}
.test-case__body {
- padding: 0 15px;
+ padding: 10px;
}
.test-case__desc {
diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js
index 42aa93a3568..f126796b0bf 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.js
@@ -112,17 +112,13 @@ export function logError(boundary: Fiber, errorInfo: CapturedValue) {
try {
logCapturedError(capturedError);
} catch (e) {
- // Prevent cycle if logCapturedError() throws.
- // A cycle may still occur if logCapturedError renders a component that throws.
- const suppressLogging = e && e.suppressReactErrorLogging;
- if (!suppressLogging) {
- // Rethrow it from a clean stack because this function is assumed to never throw.
- // We can't safely call console.error() here because it could *also* throw if overridden.
- // https://github.com/facebook/react/issues/13188
- setTimeout(() => {
- throw e;
- });
- }
+ // This method must not throw, or React internal state will get messed up.
+ // If console.error is overridden, or logCapturedError() shows a dialog that throws,
+ // we want to report this error outside of the normal stack as a last resort.
+ // https://github.com/facebook/react/issues/13188
+ setTimeout(() => {
+ throw e;
+ });
}
}
diff --git a/packages/react-reconciler/src/ReactFiberErrorLogger.js b/packages/react-reconciler/src/ReactFiberErrorLogger.js
index a5a4f756abc..81b1c7366d2 100644
--- a/packages/react-reconciler/src/ReactFiberErrorLogger.js
+++ b/packages/react-reconciler/src/ReactFiberErrorLogger.js
@@ -35,6 +35,25 @@ export function logCapturedError(capturedError: CapturedError): void {
willRetry,
} = capturedError;
+ // Browsers support silencing uncaught errors by calling
+ // `preventDefault()` in window `error` handler.
+ // We record this information as an expando on the error.
+ if (error != null && error._suppressLogging) {
+ if (errorBoundaryFound && willRetry) {
+ // The error is recoverable and was silenced.
+ // Ignore it and don't print the stack addendum.
+ // This is handy for testing error boundaries without noise.
+ return;
+ }
+ // The error is fatal. Since the silencing might have
+ // been accidental, we'll surface it anyway.
+ // However, the browser would have silenced the original error
+ // so we'll print it first, and then print the stack addendum.
+ console.error(error);
+ // For a more detailed description of this block, see:
+ // https://github.com/facebook/react/pull/13384
+ }
+
const componentNameMessage = componentName
? `The above error occurred in the <${componentName}> component:`
: 'The above error occurred in one of your React components:';
diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js
index bca85e65a6f..1231e7073f2 100644
--- a/packages/react-reconciler/src/ReactFiberScheduler.js
+++ b/packages/react-reconciler/src/ReactFiberScheduler.js
@@ -305,7 +305,19 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
isReplayingFailedUnitOfWork = false;
originalReplayError = null;
if (hasCaughtError()) {
- clearCaughtError();
+ const replayError = clearCaughtError();
+ if (replayError != null && thrownValue != null) {
+ try {
+ // Reading the expando property is intentionally
+ // inside `try` because it might be a getter or Proxy.
+ if (replayError._suppressLogging) {
+ // Also suppress logging for the original error.
+ (thrownValue: any)._suppressLogging = true;
+ }
+ } catch (inner) {
+ // Ignore.
+ }
+ }
} else {
// If the begin phase did not fail the second time, set this pointer
// back to the original value.
diff --git a/packages/shared/invokeGuardedCallback.js b/packages/shared/invokeGuardedCallback.js
index fa6ee451c91..c0cce27cc4f 100644
--- a/packages/shared/invokeGuardedCallback.js
+++ b/packages/shared/invokeGuardedCallback.js
@@ -132,6 +132,18 @@ if (__DEV__) {
if (error === null && event.colno === 0 && event.lineno === 0) {
isCrossOriginError = true;
}
+ if (event.defaultPrevented) {
+ // Some other error handler has prevented default.
+ // Browsers silence the error report if this happens.
+ // We'll remember this to later decide whether to log it or not.
+ if (error != null && typeof error === 'object') {
+ try {
+ error._suppressLogging = true;
+ } catch (inner) {
+ // Ignore.
+ }
+ }
+ }
}
// Create a fake event type.