mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
a053716077
Stacked on #28627. This makes error logging configurable using these `createRoot`/`hydrateRoot` options: ``` onUncaughtError(error: mixed, errorInfo: {componentStack?: ?string}) => void onCaughtError(error: mixed, errorInfo: {componentStack?: ?string, errorBoundary?: ?React.Component<any, any>}) => void onRecoverableError(error: mixed, errorInfo: {digest?: ?string, componentStack?: ?string}) => void ``` We already have the `onRecoverableError` option since before. Overriding these can be used to implement custom error dialogs (with access to the `componentStack`). It can also be used to silence caught errors when testing an error boundary or if you prefer not getting logs for caught errors that you've already handled in an error boundary. I currently expose the error boundary instance but I think we should probably remove that since it doesn't make sense for non-class error boundaries and isn't very useful anyway. It's also unclear what it should do when an error is rethrown from one boundary to another. Since these are public APIs now we can implement the ReactFiberErrorDialog forks using these options at the roots of the builds. So I unforked those files and instead passed a custom option for the native and www builds. To do this I had to fork the ReactDOMLegacy file into ReactDOMRootFB which is a duplication but that will go away as soon as the FB fork is the only legacy root.
230 lines
5.8 KiB
JavaScript
230 lines
5.8 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* 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
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
let React;
|
|
let ReactDOMClient;
|
|
let Scheduler;
|
|
let container;
|
|
let act;
|
|
|
|
async function fakeAct(cb) {
|
|
// We don't use act/waitForThrow here because we want to observe how errors are reported for real.
|
|
await cb();
|
|
Scheduler.unstable_flushAll();
|
|
}
|
|
|
|
describe('ReactConfigurableErrorLogging', () => {
|
|
beforeEach(() => {
|
|
jest.resetModules();
|
|
React = require('react');
|
|
ReactDOMClient = require('react-dom/client');
|
|
Scheduler = require('scheduler');
|
|
container = document.createElement('div');
|
|
if (__DEV__) {
|
|
act = React.act;
|
|
}
|
|
});
|
|
|
|
it('should log errors that occur during the begin phase', async () => {
|
|
class ErrorThrowingComponent extends React.Component {
|
|
constructor(props) {
|
|
super(props);
|
|
throw new Error('constructor error');
|
|
}
|
|
render() {
|
|
return <div />;
|
|
}
|
|
}
|
|
const uncaughtErrors = [];
|
|
const caughtErrors = [];
|
|
const root = ReactDOMClient.createRoot(container, {
|
|
onUncaughtError(error, errorInfo) {
|
|
uncaughtErrors.push(error, errorInfo);
|
|
},
|
|
onCaughtError(error, errorInfo) {
|
|
caughtErrors.push(error, errorInfo);
|
|
},
|
|
});
|
|
await fakeAct(() => {
|
|
root.render(
|
|
<div>
|
|
<span>
|
|
<ErrorThrowingComponent />
|
|
</span>
|
|
</div>,
|
|
);
|
|
});
|
|
|
|
expect(uncaughtErrors).toEqual([
|
|
expect.objectContaining({
|
|
message: 'constructor error',
|
|
}),
|
|
expect.objectContaining({
|
|
componentStack: expect.stringMatching(
|
|
new RegExp(
|
|
'\\s+(in|at) ErrorThrowingComponent (.*)\n' +
|
|
'\\s+(in|at) span(.*)\n' +
|
|
'\\s+(in|at) div(.*)',
|
|
),
|
|
),
|
|
}),
|
|
]);
|
|
expect(caughtErrors).toEqual([]);
|
|
});
|
|
|
|
it('should log errors that occur during the commit phase', async () => {
|
|
class ErrorThrowingComponent extends React.Component {
|
|
componentDidMount() {
|
|
throw new Error('componentDidMount error');
|
|
}
|
|
render() {
|
|
return <div />;
|
|
}
|
|
}
|
|
const uncaughtErrors = [];
|
|
const caughtErrors = [];
|
|
const root = ReactDOMClient.createRoot(container, {
|
|
onUncaughtError(error, errorInfo) {
|
|
uncaughtErrors.push(error, errorInfo);
|
|
},
|
|
onCaughtError(error, errorInfo) {
|
|
caughtErrors.push(error, errorInfo);
|
|
},
|
|
});
|
|
await fakeAct(() => {
|
|
root.render(
|
|
<div>
|
|
<span>
|
|
<ErrorThrowingComponent />
|
|
</span>
|
|
</div>,
|
|
);
|
|
});
|
|
|
|
expect(uncaughtErrors).toEqual([
|
|
expect.objectContaining({
|
|
message: 'componentDidMount error',
|
|
}),
|
|
expect.objectContaining({
|
|
componentStack: expect.stringMatching(
|
|
new RegExp(
|
|
'\\s+(in|at) ErrorThrowingComponent (.*)\n' +
|
|
'\\s+(in|at) span(.*)\n' +
|
|
'\\s+(in|at) div(.*)',
|
|
),
|
|
),
|
|
}),
|
|
]);
|
|
expect(caughtErrors).toEqual([]);
|
|
});
|
|
|
|
it('should ignore errors thrown in log method to prevent cycle', async () => {
|
|
class ErrorBoundary extends React.Component {
|
|
state = {error: null};
|
|
componentDidCatch(error) {
|
|
this.setState({error});
|
|
}
|
|
render() {
|
|
return this.state.error ? null : this.props.children;
|
|
}
|
|
}
|
|
class ErrorThrowingComponent extends React.Component {
|
|
render() {
|
|
throw new Error('render error');
|
|
}
|
|
}
|
|
|
|
const uncaughtErrors = [];
|
|
const caughtErrors = [];
|
|
const root = ReactDOMClient.createRoot(container, {
|
|
onUncaughtError(error, errorInfo) {
|
|
uncaughtErrors.push(error, errorInfo);
|
|
},
|
|
onCaughtError(error, errorInfo) {
|
|
caughtErrors.push(error, errorInfo);
|
|
throw new Error('onCaughtError error');
|
|
},
|
|
});
|
|
|
|
const ref = React.createRef();
|
|
|
|
await fakeAct(() => {
|
|
root.render(
|
|
<div>
|
|
<ErrorBoundary ref={ref}>
|
|
<span>
|
|
<ErrorThrowingComponent />
|
|
</span>
|
|
</ErrorBoundary>
|
|
</div>,
|
|
);
|
|
});
|
|
|
|
expect(uncaughtErrors).toEqual([]);
|
|
expect(caughtErrors).toEqual([
|
|
expect.objectContaining({
|
|
message: 'render error',
|
|
}),
|
|
expect.objectContaining({
|
|
componentStack: expect.stringMatching(
|
|
new RegExp(
|
|
'\\s+(in|at) ErrorThrowingComponent (.*)\n' +
|
|
'\\s+(in|at) span(.*)\n' +
|
|
'\\s+(in|at) ErrorBoundary(.*)\n' +
|
|
'\\s+(in|at) div(.*)',
|
|
),
|
|
),
|
|
errorBoundary: ref.current,
|
|
}),
|
|
]);
|
|
|
|
// The error thrown in caughtError should be rethrown with a clean stack
|
|
expect(() => {
|
|
jest.runAllTimers();
|
|
}).toThrow('onCaughtError error');
|
|
});
|
|
|
|
it('does not log errors when inside real act', async () => {
|
|
function ErrorThrowingComponent() {
|
|
throw new Error('render error');
|
|
}
|
|
const uncaughtErrors = [];
|
|
const caughtErrors = [];
|
|
const root = ReactDOMClient.createRoot(container, {
|
|
onUncaughtError(error, errorInfo) {
|
|
uncaughtErrors.push(error, errorInfo);
|
|
},
|
|
onCaughtError(error, errorInfo) {
|
|
caughtErrors.push(error, errorInfo);
|
|
},
|
|
});
|
|
|
|
if (__DEV__) {
|
|
global.IS_REACT_ACT_ENVIRONMENT = true;
|
|
|
|
await expect(async () => {
|
|
await act(() => {
|
|
root.render(
|
|
<div>
|
|
<span>
|
|
<ErrorThrowingComponent />
|
|
</span>
|
|
</div>,
|
|
);
|
|
});
|
|
}).rejects.toThrow('render error');
|
|
}
|
|
|
|
expect(uncaughtErrors).toEqual([]);
|
|
expect(caughtErrors).toEqual([]);
|
|
});
|
|
});
|