-
Notifications
You must be signed in to change notification settings - Fork 47.4k
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
Replace console.error() with a throw in setTimeout() as last resort exception logging #13310
Changes from all commits
45ac1dd
0db4582
6fc3e18
9dfa482
f703123
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
/** | ||
* Copyright (c) 2013-present, Facebook, Inc. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
* | ||
* @emails react-core | ||
* @jest-environment node | ||
*/ | ||
|
||
// This is a regression test for https://github.com/facebook/react/issues/13188. | ||
// It reproduces a combination of conditions that led to a problem. | ||
|
||
if (global.window) { | ||
throw new Error('This test must run in a Node environment.'); | ||
} | ||
|
||
// The issue only reproduced when React was loaded before JSDOM. | ||
const React = require('react'); | ||
const ReactDOM = require('react-dom'); | ||
|
||
// Unlike other tests, we want to enable error logging. | ||
// Note this is not a real Error prototype property, | ||
// it's only set in our Jest environment. | ||
// eslint-disable-next-line no-extend-native | ||
Error.prototype.suppressReactErrorLogging = false; | ||
|
||
// Initialize JSDOM separately. | ||
// We don't use our normal JSDOM setup because we want to load React first. | ||
const {JSDOM} = require('jsdom'); | ||
global.requestAnimationFrame = setTimeout; | ||
global.cancelAnimationFrame = clearTimeout; | ||
const jsdom = new JSDOM(`<div id="app-root"></div>`); | ||
global.window = jsdom.window; | ||
global.document = jsdom.window.document; | ||
global.navigator = jsdom.window.navigator; | ||
|
||
class Bad extends React.Component { | ||
componentDidUpdate() { | ||
throw new Error('no'); | ||
} | ||
render() { | ||
return null; | ||
} | ||
} | ||
|
||
describe('ReactErrorLoggingRecovery', () => { | ||
let originalConsoleError = console.error; | ||
|
||
beforeEach(() => { | ||
console.error = error => { | ||
throw new Error('Buggy console.error'); | ||
}; | ||
}); | ||
|
||
afterEach(() => { | ||
console.error = originalConsoleError; | ||
}); | ||
|
||
it('should recover from errors in console.error', function() { | ||
const div = document.createElement('div'); | ||
let didCatch = false; | ||
try { | ||
ReactDOM.render(<Bad />, div); | ||
ReactDOM.render(<Bad />, div); | ||
} catch (e) { | ||
expect(e.message).toBe('no'); | ||
didCatch = true; | ||
} | ||
expect(didCatch).toBe(true); | ||
ReactDOM.render(<span>Hello</span>, div); | ||
expect(div.firstChild.textContent).toBe('Hello'); | ||
|
||
// Verify the console.error bug is surfaced | ||
expect(() => { | ||
jest.runAllTimers(); | ||
}).toThrow('Buggy console.error'); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -116,7 +116,12 @@ export function logError(boundary: Fiber, errorInfo: CapturedValue<mixed>) { | |
// A cycle may still occur if logCapturedError renders a component that throws. | ||
const suppressLogging = e && e.suppressReactErrorLogging; | ||
if (!suppressLogging) { | ||
console.error(e); | ||
// 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 overriden. | ||
// https://github.com/facebook/react/issues/13188 | ||
setTimeout(() => { | ||
throw e; | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since you set There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't really intend to test this part although I guess I could. I only wanted to ensure that we don't blow up the reconciler by rethrowing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a check. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool, my primary concern was just confusion in test reporting if the error showed up randomly in the test suite because Node or Jest saw an uncaught error. No need to chase this down too much. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah cool. Posted too late! I didn't realize that fake timers were already setup, so this wouldn't have run anyway. |
||
} | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess
setTimeout
could also be buggy. Let's not think about this.