@samitouri / QOS-React-2 / commits / 3938ccc88a

Allow the user to opt out of seeing "The above error..." addendum (#13384)

* Remove e.suppressReactErrorLogging check before last resort throw It's unnecessary here. It was here because this method called console.error(). But we now rethrow with a clean stack, and that's worth doing regardless of whether the logging is silenced. * Don't print error addendum if 'error' event got preventDefault() * Add fixtures * Use an expando property instead of a WeakSet * Make it a bit less fragile * Clarify comments

Dan Abramov committed Aug 13, 2018 at 21:33 UTC 3938ccc88aa3dcc5a4460474bda40af97dd6e234
6 files changed +213 -15
fixtures/dom/src/components/fixtures/error-handling/index.js
+161 -2
@@ -7,9 +7,21 @@ const ReactDOM = window.ReactDOM;
7 function BadRender(props) {
8 props.doThrow();
9 }
10 +
11 +class BadDidMount extends React.Component {
12 + componentDidMount() {
13 + this.props.doThrow();
14 + }
15 +
16 + render() {
17 + return null;
18 + }
19 +}
20 +
21 class ErrorBoundary extends React.Component {
22 static defaultProps = {
23 buttonText: 'Trigger error',
24 + badChildType: BadRender,
25 };
26 state = {
27 shouldThrow: false,
@@ -33,7 +45,8 @@ class ErrorBoundary extends React.Component {
45 }
46 }
47 if (this.state.shouldThrow) {
36 - return <BadRender doThrow={this.props.doThrow} />;
48 + const BadChild = this.props.badChildType;
49 + return <BadChild doThrow={this.props.doThrow} />;
50 }
51 return <button onClick={this.triggerError}>{this.props.buttonText}</button>;
52 }
@@ -84,6 +97,112 @@ class TriggerErrorAndCatch extends React.Component {
97 }
98 }
99
100 +function silenceWindowError(event) {
101 + event.preventDefault();
102 +}
103 +
104 +class SilenceErrors extends React.Component {
105 + state = {
106 + silenceErrors: false,
107 + };
108 + componentDidMount() {
109 + if (this.state.silenceErrors) {
110 + window.addEventListener('error', silenceWindowError);
111 + }
112 + }
113 + componentDidUpdate(prevProps, prevState) {
114 + if (!prevState.silenceErrors && this.state.silenceErrors) {
115 + window.addEventListener('error', silenceWindowError);
116 + } else if (prevState.silenceErrors && !this.state.silenceErrors) {
117 + window.removeEventListener('error', silenceWindowError);
118 + }
119 + }
120 + componentWillUnmount() {
121 + if (this.state.silenceErrors) {
122 + window.removeEventListener('error', silenceWindowError);
123 + }
124 + }
125 + render() {
126 + return (
127 + <div>
128 + <label>
129 + <input
130 + type="checkbox"
131 + value={this.state.silenceErrors}
132 + onChange={() =>
133 + this.setState(state => ({
134 + silenceErrors: !state.silenceErrors,
135 + }))
136 + }
137 + />
138 + Silence errors
139 + </label>
140 + {this.state.silenceErrors && (
141 + <div>
142 + {this.props.children}
143 + <br />
144 + <hr />
145 + <b style={{color: 'red'}}>
146 + Don't forget to uncheck "Silence errors" when you're done with
147 + this test!
148 + </b>
149 + </div>
150 + )}
151 + </div>
152 + );
153 + }
154 +}
155 +
156 +class SilenceRecoverableError extends React.Component {
157 + render() {
158 + return (
159 + <SilenceErrors>
160 + <ErrorBoundary
161 + badChildType={BadRender}
162 + buttonText={'Throw (render phase)'}
163 + doThrow={() => {
164 + throw new Error('Silenced error (render phase)');
165 + }}
166 + />
167 + <ErrorBoundary
168 + badChildType={BadDidMount}
169 + buttonText={'Throw (commit phase)'}
170 + doThrow={() => {
171 + throw new Error('Silenced error (commit phase)');
172 + }}
173 + />
174 + </SilenceErrors>
175 + );
176 + }
177 +}
178 +
179 +class TrySilenceFatalError extends React.Component {
180 + container = document.createElement('div');
181 +
182 + triggerErrorAndCatch = () => {
183 + try {
184 + ReactDOM.flushSync(() => {
185 + ReactDOM.render(
186 + <BadRender
187 + doThrow={() => {
188 + throw new Error('Caught error');
189 + }}
190 + />,
191 + this.container
192 + );
193 + });
194 + } catch (e) {}
195 + };
196 +
197 + render() {
198 + return (
199 + <SilenceErrors>
200 + <button onClick={this.triggerErrorAndCatch}>Throw fatal error</button>
201 + </SilenceErrors>
202 + );
203 + }
204 +}
205 +
206 export default class ErrorHandlingTestCases extends React.Component {
207 render() {
208 return (
@@ -103,6 +222,12 @@ export default class ErrorHandlingTestCases extends React.Component {
222 the BadRender component. After resuming, the "Trigger error" button
223 should be replaced with "Captured an error: Oops!" Clicking reset
224 should reset the test case.
225 + <br />
226 + <br />
227 + In the console, you should see <b>two</b> messages: the actual error
228 + ("Oops") printed natively by the browser with its JavaScript stack,
229 + and our addendum ("The above error occurred in BadRender component")
230 + with a React component stack.
231 </TestCase.ExpectedResult>
232 <Example
233 doThrow={() => {
@@ -155,10 +280,44 @@ export default class ErrorHandlingTestCases extends React.Component {
280 </TestCase.Steps>
281 <TestCase.ExpectedResult>
282 Open the console. "Uncaught Error: Caught error" should have been
158 - logged by the browser.
283 + logged by the browser. You should also see our addendum ("The above
284 + error...").
285 </TestCase.ExpectedResult>
286 <TriggerErrorAndCatch />
287 </TestCase>
288 + <TestCase
289 + title="Recoverable errors can be silenced with preventDefault (development mode only)"
290 + description="">
291 + <TestCase.Steps>
292 + <li>Check the "Silence errors" checkbox below</li>
293 + <li>Click the "Throw (render phase)" button</li>
294 + <li>Click the "Throw (commit phase)" button</li>
295 + <li>Uncheck the "Silence errors" checkbox</li>
296 + </TestCase.Steps>
297 + <TestCase.ExpectedResult>
298 + Open the console. You shouldn't see <b>any</b> messages in the
299 + console: neither the browser error, nor our "The above error"
300 + addendum, from either of the buttons. The buttons themselves should
301 + get replaced by two labels: "Captured an error: Silenced error
302 + (render phase)" and "Captured an error: Silenced error (commit
303 + phase)".
304 + </TestCase.ExpectedResult>
305 + <SilenceRecoverableError />
306 + </TestCase>
307 + <TestCase
308 + title="Fatal errors cannot be silenced with preventDefault (development mode only)"
309 + description="">
310 + <TestCase.Steps>
311 + <li>Check the "Silence errors" checkbox below</li>
312 + <li>Click the "Throw fatal error" button</li>
313 + <li>Uncheck the "Silence errors" checkbox</li>
314 + </TestCase.Steps>
315 + <TestCase.ExpectedResult>
316 + Open the console. "Error: Caught error" should have been logged by
317 + React. You should also see our addendum ("The above error...").
318 + </TestCase.ExpectedResult>
319 + <TrySilenceFatalError />
320 + </TestCase>
321 </FixtureSet>
322 );
323 }
fixtures/dom/src/style.css
+1 -1
@@ -158,7 +158,7 @@ li {
158 }
159
160 .test-case__body {
161 - padding: 0 15px;
161 + padding: 10px;
162 }
163
164 .test-case__desc {
packages/react-reconciler/src/ReactFiberCommitWork.js
+7 -11
@@ -112,17 +112,13 @@ export function logError(boundary: Fiber, errorInfo: CapturedValue<mixed>) {
112 try {
113 logCapturedError(capturedError);
114 } catch (e) {
115 - // Prevent cycle if logCapturedError() throws.
116 - // A cycle may still occur if logCapturedError renders a component that throws.
117 - const suppressLogging = e && e.suppressReactErrorLogging;
118 - if (!suppressLogging) {
119 - // Rethrow it from a clean stack because this function is assumed to never throw.
120 - // We can't safely call console.error() here because it could *also* throw if overridden.
121 - // https://github.com/facebook/react/issues/13188
122 - setTimeout(() => {
123 - throw e;
124 - });
125 - }
115 + // This method must not throw, or React internal state will get messed up.
116 + // If console.error is overridden, or logCapturedError() shows a dialog that throws,
117 + // we want to report this error outside of the normal stack as a last resort.
118 + // https://github.com/facebook/react/issues/13188
119 + setTimeout(() => {
120 + throw e;
121 + });
122 }
123 }
124
packages/react-reconciler/src/ReactFiberErrorLogger.js
+19
@@ -35,6 +35,25 @@ export function logCapturedError(capturedError: CapturedError): void {
35 willRetry,
36 } = capturedError;
37
38 + // Browsers support silencing uncaught errors by calling
39 + // `preventDefault()` in window `error` handler.
40 + // We record this information as an expando on the error.
41 + if (error != null && error._suppressLogging) {
42 + if (errorBoundaryFound && willRetry) {
43 + // The error is recoverable and was silenced.
44 + // Ignore it and don't print the stack addendum.
45 + // This is handy for testing error boundaries without noise.
46 + return;
47 + }
48 + // The error is fatal. Since the silencing might have
49 + // been accidental, we'll surface it anyway.
50 + // However, the browser would have silenced the original error
51 + // so we'll print it first, and then print the stack addendum.
52 + console.error(error);
53 + // For a more detailed description of this block, see:
54 + // https://github.com/facebook/react/pull/13384
55 + }
56 +
57 const componentNameMessage = componentName
58 ? `The above error occurred in the <${componentName}> component:`
59 : 'The above error occurred in one of your React components:';
packages/react-reconciler/src/ReactFiberScheduler.js
+13 -1
@@ -305,7 +305,19 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
305 isReplayingFailedUnitOfWork = false;
306 originalReplayError = null;
307 if (hasCaughtError()) {
308 - clearCaughtError();
308 + const replayError = clearCaughtError();
309 + if (replayError != null && thrownValue != null) {
310 + try {
311 + // Reading the expando property is intentionally
312 + // inside `try` because it might be a getter or Proxy.
313 + if (replayError._suppressLogging) {
314 + // Also suppress logging for the original error.
315 + (thrownValue: any)._suppressLogging = true;
316 + }
317 + } catch (inner) {
318 + // Ignore.
319 + }
320 + }
321 } else {
322 // If the begin phase did not fail the second time, set this pointer
323 // back to the original value.
packages/shared/invokeGuardedCallback.js
+12
@@ -132,6 +132,18 @@ if (__DEV__) {
132 if (error === null && event.colno === 0 && event.lineno === 0) {
133 isCrossOriginError = true;
134 }
135 + if (event.defaultPrevented) {
136 + // Some other error handler has prevented default.
137 + // Browsers silence the error report if this happens.
138 + // We'll remember this to later decide whether to log it or not.
139 + if (error != null && typeof error === 'object') {
140 + try {
141 + error._suppressLogging = true;
142 + } catch (inner) {
143 + // Ignore.
144 + }
145 + }
146 + }
147 }
148
149 // Create a fake event type.