/*** 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* @jest-environment node*/'use strict';
let ReactFeatureFlags = require('shared/ReactFeatureFlags');
let PropTypes;
let React;
let ReactNoop;
let Scheduler;
let act;
let assertLog;
let waitForAll;
let waitFor;
let waitForThrow;
describe('ReactIncrementalErrorHandling', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
PropTypes = require('prop-types');
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
act = require('internal-test-utils').act;
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
waitForAll = InternalTestUtils.waitForAll;
waitFor = InternalTestUtils.waitFor;
waitForThrow = InternalTestUtils.waitForThrow;
});afterEach(() => {
jest.restoreAllMocks();
});function normalizeCodeLocInfo(str) {
return (
str &&
str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) {
return '\n in ' + name + ' (at **)';
}));}// Note: This is based on a similar component we use in www. We can delete
// once the extra div wrapper is no longer necessary.
function LegacyHiddenDiv({children, mode}) {
return (
<div hidden={mode === 'hidden'}>
<React.unstable_LegacyHidden
mode={mode === 'hidden' ? 'unstable-defer-without-hiding' : mode}>
{children}
</React.unstable_LegacyHidden>
</div>
);
}it('recovers from errors asynchronously', async () => {
class ErrorBoundary extends React.Component {
state = {error: null};
static getDerivedStateFromError(error) {
Scheduler.log('getDerivedStateFromError');
return {error};
}render() {
if (this.state.error) {
Scheduler.log('ErrorBoundary (catch)');
return <ErrorMessage error={this.state.error} />;
}Scheduler.log('ErrorBoundary (try)');
return this.props.children;
}}function ErrorMessage({error}) {
Scheduler.log('ErrorMessage');
return <span prop={`Caught an error: ${error.message}`} />;
}function Indirection({children}) {Scheduler.log('Indirection');return children || null;}function BadRender({unused}) {Scheduler.log('throw');throw new Error('oops!');}React.startTransition(() => {ReactNoop.render(<><ErrorBoundary><Indirection><Indirection><Indirection><BadRender /></Indirection></Indirection></Indirection></ErrorBoundary><Indirection /><Indirection /></>,);});// Start rendering asynchronouslyawait waitFor(['ErrorBoundary (try)','Indirection','Indirection','Indirection',// An error is thrown. React keeps rendering asynchronously.'throw',// Call getDerivedStateFromError and re-render the error boundary, this// time rendering an error message.'getDerivedStateFromError','ErrorBoundary (catch)','ErrorMessage',]);expect(ReactNoop).toMatchRenderedOutput(null);// The work loop unwound to the nearest error boundary. Continue rendering// asynchronously.await waitFor(['Indirection']);// Since the error was thrown during an async render, React won't commit the// result yet. After render we render the last child, React will attempt to// render again, synchronously, just in case that happens to fix the error// (i.e. as in the case of a data race). Flush just one more unit of work to// demonstrate that this render is synchronous.expect(ReactNoop.flushNextYield()).toEqual(['Indirection','ErrorBoundary (try)','Indirection','Indirection','Indirection',// The error was thrown again. This time, React will actually commit// the result.'throw','getDerivedStateFromError','ErrorBoundary (catch)','ErrorMessage','Indirection','Indirection',]);expect(ReactNoop).toMatchRenderedOutput(<span prop="Caught an error: oops!" />,);});it('recovers from errors asynchronously (legacy, no getDerivedStateFromError)', async () => {class ErrorBoundary extends React.Component {state = {error: null};componentDidCatch(error) {Scheduler.log('componentDidCatch');this.setState({error});}render() {if (this.state.error) {Scheduler.log('ErrorBoundary (catch)');return <ErrorMessage error={this.state.error} />;}Scheduler.log('ErrorBoundary (try)');return this.props.children;}}function ErrorMessage({error}) {Scheduler.log('ErrorMessage');return <span prop={`Caught an error: ${error.message}`} />;
}function Indirection({children}) {Scheduler.log('Indirection');return children || null;}function BadRender({unused}) {Scheduler.log('throw');throw new Error('oops!');}React.startTransition(() => {ReactNoop.render(<><ErrorBoundary><Indirection><Indirection><Indirection><BadRender /></Indirection></Indirection></Indirection></ErrorBoundary><Indirection /><Indirection /></>,);});// Start rendering asynchronouslyawait waitFor(['ErrorBoundary (try)','Indirection','Indirection','Indirection',// An error is thrown. React keeps rendering asynchronously.'throw',]);// Still rendering async...await waitFor(['Indirection']);await waitFor(['Indirection',// Now that the tree is complete, and there's no remaining work, React// reverts to legacy mode to retry one more time before handling the error.'ErrorBoundary (try)','Indirection','Indirection','Indirection',// The error was thrown again. Now we can handle it.'throw','Indirection','Indirection','componentDidCatch','ErrorBoundary (catch)','ErrorMessage',]);expect(ReactNoop).toMatchRenderedOutput(<span prop="Caught an error: oops!" />,);});it("retries at a lower priority if there's additional pending work", async () => {function App(props) {if (props.isBroken) {Scheduler.log('error');throw new Error('Oops!');}Scheduler.log('success');return <span prop="Everything is fine." />;}function onCommit() {Scheduler.log('commit');}React.startTransition(() => {ReactNoop.render(<App isBroken={true} />, onCommit);});await waitFor(['error']);React.startTransition(() => {// This update is in a separate batchReactNoop.render(<App isBroken={false} />, onCommit);});// React will try to recover by rendering all the pending updates in a// single batch, synchronously. This time it succeeds.//// This tells Scheduler to render a single unit of work. Because the render// to recover from the error is synchronous, this should be enough to// finish the rest of the work.Scheduler.unstable_flushNumberOfYields(1);assertLog(['success',// Nothing commits until the second update completes.'commit','commit',]);expect(ReactNoop).toMatchRenderedOutput(<span prop="Everything is fine." />,);});// @gate wwwit('does not include offscreen work when retrying after an error', async () => {function App(props) {if (props.isBroken) {Scheduler.log('error');throw new Error('Oops!');}Scheduler.log('success');return (<>Everything is fine<LegacyHiddenDiv mode="hidden"><div>Offscreen content</div></LegacyHiddenDiv></>);}function onCommit() {Scheduler.log('commit');}React.startTransition(() => {ReactNoop.render(<App isBroken={true} />, onCommit);});await waitFor(['error']);expect(ReactNoop).toMatchRenderedOutput(null);React.startTransition(() => {// This update is in a separate batchReactNoop.render(<App isBroken={false} />, onCommit);});// React will try to recover by rendering all the pending updates in a// single batch, synchronously. This time it succeeds.//// This tells Scheduler to render a single unit of work. Because the render// to recover from the error is synchronous, this should be enough to// finish the rest of the work.Scheduler.unstable_flushNumberOfYields(1);assertLog(['success',// Nothing commits until the second update completes.'commit','commit',]);// This should not include the offscreen contentexpect(ReactNoop).toMatchRenderedOutput(<>Everything is fine<div hidden={true} /></>,);// The offscreen content finishes in a subsequent renderawait waitForAll([]);expect(ReactNoop).toMatchRenderedOutput(<>Everything is fine<div hidden={true}><div>Offscreen content</div></div></>,);});it('retries one more time before handling error', async () => {function BadRender({unused}) {Scheduler.log('BadRender');throw new Error('oops');}function Sibling({unused}) {Scheduler.log('Sibling');return <span prop="Sibling" />;}function Parent({unused}) {Scheduler.log('Parent');return (<><BadRender /><Sibling /></>);}React.startTransition(() => {ReactNoop.render(<Parent />, () => Scheduler.log('commit'));});// Render the bad component asynchronouslyawait waitFor(['Parent', 'BadRender']);// The work loop unwound to the nearest error boundary. React will try// to render one more time, synchronously. Flush just one unit of work to// demonstrate that this render is synchronous.expect(() => Scheduler.unstable_flushNumberOfYields(1)).toThrow('oops');assertLog(['Parent', 'BadRender', 'commit']);expect(ReactNoop).toMatchRenderedOutput(null);});it('retries one more time if an error occurs during a render that expires midway through the tree', async () => {function Oops({unused}) {Scheduler.log('Oops');throw new Error('Oops');}function Text({text}) {Scheduler.log(text);return text;}function App({unused}) {return (<><Text text="A" /><Text text="B" /><Oops /><Text text="C" /><Text text="D" /></>);}React.startTransition(() => {ReactNoop.render(<App />);});// Render part of the treeawait waitFor(['A', 'B']);// Expire the render midway throughScheduler.unstable_advanceTime(10000);expect(() => {Scheduler.unstable_flushExpired();ReactNoop.flushSync();}).toThrow('Oops');assertLog([// The render expired, but we shouldn't throw out the partial work.// Finish the current level.'Oops',// Since the error occurred during a partially concurrent render, we should// retry one more time, synchronously.'A','B','Oops',]);expect(ReactNoop).toMatchRenderedOutput(null);});it('calls componentDidCatch multiple times for multiple errors', async () => {let id = 0;class BadMount extends React.Component {componentDidMount() {throw new Error(`Error ${++id}`);
}render() {Scheduler.log('BadMount');return null;}}class ErrorBoundary extends React.Component {state = {errorCount: 0};componentDidCatch(error) {Scheduler.log(`componentDidCatch: ${error.message}`);
this.setState(state => ({errorCount: state.errorCount + 1}));}render() {if (this.state.errorCount > 0) {return <span prop={`Number of errors: ${this.state.errorCount}`} />;
}Scheduler.log('ErrorBoundary');return this.props.children;}}ReactNoop.render(<ErrorBoundary><BadMount /><BadMount /><BadMount /></ErrorBoundary>,);await waitForAll(['ErrorBoundary','BadMount','BadMount','BadMount','componentDidCatch: Error 1','componentDidCatch: Error 2','componentDidCatch: Error 3',]);expect(ReactNoop).toMatchRenderedOutput(<span prop="Number of errors: 3" />,);});it('catches render error in a boundary during full deferred mounting', async () => {class ErrorBoundary extends React.Component {state = {error: null};componentDidCatch(error) {this.setState({error});}render() {if (this.state.error) {return (<span prop={`Caught an error: ${this.state.error.message}.`} />
);}return this.props.children;}}function BrokenRender(props) {throw new Error('Hello');}ReactNoop.render(<ErrorBoundary><BrokenRender /></ErrorBoundary>,);await waitForAll([]);expect(ReactNoop).toMatchRenderedOutput(<span prop="Caught an error: Hello." />,);});it('catches render error in a boundary during partial deferred mounting', async () => {class ErrorBoundary extends React.Component {state = {error: null};componentDidCatch(error) {Scheduler.log('ErrorBoundary componentDidCatch');this.setState({error});}render() {if (this.state.error) {Scheduler.log('ErrorBoundary render error');return (<span prop={`Caught an error: ${this.state.error.message}.`} />
);}Scheduler.log('ErrorBoundary render success');return this.props.children;}}function BrokenRender({unused}) {Scheduler.log('BrokenRender');throw new Error('Hello');}React.startTransition(() => {ReactNoop.render(<ErrorBoundary><BrokenRender /></ErrorBoundary>,);});await waitFor(['ErrorBoundary render success']);expect(ReactNoop).toMatchRenderedOutput(null);await waitForAll(['BrokenRender',// React retries one more time'ErrorBoundary render success',// Errored again on retry. Now handle it.'BrokenRender','ErrorBoundary componentDidCatch','ErrorBoundary render error',]);expect(ReactNoop).toMatchRenderedOutput(<span prop="Caught an error: Hello." />,);});it('catches render error in a boundary during synchronous mounting', () => {class ErrorBoundary extends React.Component {state = {error: null};componentDidCatch(error) {Scheduler.log('ErrorBoundary componentDidCatch');this.setState({error});}render() {if (this.state.error) {Scheduler.log('ErrorBoundary render error');return (<span prop={`Caught an error: ${this.state.error.message}.`} />
);}Scheduler.log('ErrorBoundary render success');return this.props.children;}}function BrokenRender({unused}) {Scheduler.log('BrokenRender');throw new Error('Hello');}ReactNoop.flushSync(() => {ReactNoop.render(<ErrorBoundary><BrokenRender /></ErrorBoundary>,);});assertLog(['ErrorBoundary render success','BrokenRender',// React retries one more time'ErrorBoundary render success','BrokenRender',// Errored again on retry. Now handle it.'ErrorBoundary componentDidCatch','ErrorBoundary render error',]);expect(ReactNoop).toMatchRenderedOutput(<span prop="Caught an error: Hello." />,);});it('catches render error in a boundary during batched mounting', () => {class ErrorBoundary extends React.Component {state = {error: null};componentDidCatch(error) {Scheduler.log('ErrorBoundary componentDidCatch');this.setState({error});}render() {if (this.state.error) {Scheduler.log('ErrorBoundary render error');return (<span prop={`Caught an error: ${this.state.error.message}.`} />
);}Scheduler.log('ErrorBoundary render success');return this.props.children;}}function BrokenRender({unused}) {Scheduler.log('BrokenRender');throw new Error('Hello');}ReactNoop.flushSync(() => {ReactNoop.render(<ErrorBoundary>Before the storm.</ErrorBoundary>);ReactNoop.render(<ErrorBoundary><BrokenRender /></ErrorBoundary>,);});assertLog(['ErrorBoundary render success','BrokenRender',// React retries one more time'ErrorBoundary render success','BrokenRender',// Errored again on retry. Now handle it.'ErrorBoundary componentDidCatch','ErrorBoundary render error',]);expect(ReactNoop).toMatchRenderedOutput(<span prop="Caught an error: Hello." />,);});it('propagates an error from a noop error boundary during full deferred mounting', async () => {class RethrowErrorBoundary extends React.Component {componentDidCatch(error) {Scheduler.log('RethrowErrorBoundary componentDidCatch');throw error;}render() {Scheduler.log('RethrowErrorBoundary render');return this.props.children;}}function BrokenRender({unused}) {Scheduler.log('BrokenRender');throw new Error('Hello');}ReactNoop.render(<RethrowErrorBoundary><BrokenRender /></RethrowErrorBoundary>,);await waitForThrow('Hello');assertLog(['RethrowErrorBoundary render','BrokenRender',// React retries one more time'RethrowErrorBoundary render','BrokenRender',// Errored again on retry. Now handle it.'RethrowErrorBoundary componentDidCatch',]);expect(ReactNoop.getChildrenAsJSX()).toEqual(null);});it('propagates an error from a noop error boundary during partial deferred mounting', async () => {class RethrowErrorBoundary extends React.Component {componentDidCatch(error) {Scheduler.log('RethrowErrorBoundary componentDidCatch');throw error;}render() {Scheduler.log('RethrowErrorBoundary render');return this.props.children;}}function BrokenRender({unused}) {Scheduler.log('BrokenRender');throw new Error('Hello');}React.startTransition(() => {ReactNoop.render(<RethrowErrorBoundary><BrokenRender /></RethrowErrorBoundary>,);});await waitFor(['RethrowErrorBoundary render']);await waitForThrow('Hello');assertLog(['BrokenRender',// React retries one more time'RethrowErrorBoundary render','BrokenRender',// Errored again on retry. Now handle it.'RethrowErrorBoundary componentDidCatch',]);expect(ReactNoop).toMatchRenderedOutput(null);});it('propagates an error from a noop error boundary during synchronous mounting', () => {class RethrowErrorBoundary extends React.Component {componentDidCatch(error) {Scheduler.log('RethrowErrorBoundary componentDidCatch');throw error;}render() {Scheduler.log('RethrowErrorBoundary render');return this.props.children;}}function BrokenRender({unused}) {Scheduler.log('BrokenRender');throw new Error('Hello');}expect(() => {ReactNoop.flushSync(() => {ReactNoop.render(<RethrowErrorBoundary><BrokenRender /></RethrowErrorBoundary>,);});}).toThrow('Hello');assertLog(['RethrowErrorBoundary render','BrokenRender',// React retries one more time'RethrowErrorBoundary render','BrokenRender',// Errored again on retry. Now handle it.'RethrowErrorBoundary componentDidCatch',]);expect(ReactNoop).toMatchRenderedOutput(null);});it('propagates an error from a noop error boundary during batched mounting', () => {class RethrowErrorBoundary extends React.Component {componentDidCatch(error) {Scheduler.log('RethrowErrorBoundary componentDidCatch');throw error;}render() {Scheduler.log('RethrowErrorBoundary render');return this.props.children;}}function BrokenRender({unused}) {Scheduler.log('BrokenRender');throw new Error('Hello');}expect(() => {ReactNoop.flushSync(() => {ReactNoop.render(<RethrowErrorBoundary>Before the storm.</RethrowErrorBoundary>,);ReactNoop.render(<RethrowErrorBoundary><BrokenRender /></RethrowErrorBoundary>,);});}).toThrow('Hello');assertLog(['RethrowErrorBoundary render','BrokenRender',// React retries one more time'RethrowErrorBoundary render','BrokenRender',// Errored again on retry. Now handle it.'RethrowErrorBoundary componentDidCatch',]);expect(ReactNoop).toMatchRenderedOutput(null);});it('applies batched updates regardless despite errors in scheduling', async () => {ReactNoop.render(<span prop="a:1" />);expect(() => {ReactNoop.batchedUpdates(() => {ReactNoop.render(<span prop="a:2" />);ReactNoop.render(<span prop="a:3" />);throw new Error('Hello');});}).toThrow('Hello');await waitForAll([]);expect(ReactNoop).toMatchRenderedOutput(<span prop="a:3" />);});it('applies nested batched updates despite errors in scheduling', async () => {ReactNoop.render(<span prop="a:1" />);expect(() => {ReactNoop.batchedUpdates(() => {ReactNoop.render(<span prop="a:2" />);ReactNoop.render(<span prop="a:3" />);ReactNoop.batchedUpdates(() => {ReactNoop.render(<span prop="a:4" />);ReactNoop.render(<span prop="a:5" />);throw new Error('Hello');});});}).toThrow('Hello');await waitForAll([]);expect(ReactNoop).toMatchRenderedOutput(<span prop="a:5" />);});// TODO: Is this a breaking change?it('defers additional sync work to a separate event after an error', async () => {ReactNoop.render(<span prop="a:1" />);expect(() => {ReactNoop.flushSync(() => {ReactNoop.batchedUpdates(() => {ReactNoop.render(<span prop="a:2" />);ReactNoop.render(<span prop="a:3" />);throw new Error('Hello');});});}).toThrow('Hello');await waitForAll([]);expect(ReactNoop).toMatchRenderedOutput(<span prop="a:3" />);});it('can schedule updates after uncaught error in render on mount', async () => {function BrokenRender({unused}) {Scheduler.log('BrokenRender');throw new Error('Hello');}function Foo({unused}) {Scheduler.log('Foo');return null;}ReactNoop.render(<BrokenRender />);await waitForThrow('Hello');ReactNoop.render(<Foo />);assertLog(['BrokenRender',// React retries one more time'BrokenRender',// Errored again on retry]);await waitForAll(['Foo']);});it('can schedule updates after uncaught error in render on update', async () => {function BrokenRender({shouldThrow}) {Scheduler.log('BrokenRender');if (shouldThrow) {throw new Error('Hello');}return null;}function Foo({unused}) {Scheduler.log('Foo');return null;}ReactNoop.render(<BrokenRender shouldThrow={false} />);await waitForAll(['BrokenRender']);ReactNoop.render(<BrokenRender shouldThrow={true} />);await waitForThrow('Hello');assertLog(['BrokenRender',// React retries one more time'BrokenRender',// Errored again on retry]);ReactNoop.render(<Foo />);await waitForAll(['Foo']);});it('can schedule updates after uncaught error during unmounting', async () => {class BrokenComponentWillUnmount extends React.Component {render() {return <div />;}componentWillUnmount() {throw new Error('Hello');}}function Foo() {Scheduler.log('Foo');return null;}ReactNoop.render(<BrokenComponentWillUnmount />);await waitForAll([]);ReactNoop.render(<div />);await waitForThrow('Hello');ReactNoop.render(<Foo />);await waitForAll(['Foo']);});it('should not attempt to recover an unmounting error boundary', async () => {class Parent extends React.Component {componentWillUnmount() {Scheduler.log('Parent componentWillUnmount');}render() {return <Boundary />;}}class Boundary extends React.Component {componentDidCatch(e) {Scheduler.log(`Caught error: ${e.message}`);
}render() {return <ThrowsOnUnmount />;}}class ThrowsOnUnmount extends React.Component {componentWillUnmount() {Scheduler.log('ThrowsOnUnmount componentWillUnmount');throw new Error('unmount error');}render() {return null;}}ReactNoop.render(<Parent />);await waitForAll([]);// Because the error boundary is also unmounting,// an error in ThrowsOnUnmount should be rethrown.ReactNoop.render(null);await waitForThrow('unmount error');await assertLog(['Parent componentWillUnmount','ThrowsOnUnmount componentWillUnmount',]);ReactNoop.render(<Parent />);});it('can unmount an error boundary before it is handled', async () => {let parent;class Parent extends React.Component {state = {step: 0};render() {parent = this;return this.state.step === 0 ? <Boundary /> : null;}}class Boundary extends React.Component {componentDidCatch() {}render() {return <Child />;}}class Child extends React.Component {componentDidUpdate() {parent.setState({step: 1});throw new Error('update error');}render() {return null;}}ReactNoop.render(<Parent />);await waitForAll([]);ReactNoop.flushSync(() => {ReactNoop.render(<Parent />);});});it('continues work on other roots despite caught errors', async () => {class ErrorBoundary extends React.Component {state = {error: null};componentDidCatch(error) {this.setState({error});}render() {if (this.state.error) {return (<span prop={`Caught an error: ${this.state.error.message}.`} />
);}return this.props.children;}}function BrokenRender(props) {throw new Error('Hello');}ReactNoop.renderToRootWithID(<ErrorBoundary><BrokenRender /></ErrorBoundary>,'a',);ReactNoop.renderToRootWithID(<span prop="b:1" />, 'b');await waitForAll([]);expect(ReactNoop.getChildrenAsJSX('a')).toEqual(<span prop="Caught an error: Hello." />,);await waitForAll([]);expect(ReactNoop.getChildrenAsJSX('b')).toEqual(<span prop="b:1" />);});it('continues work on other roots despite uncaught errors', async () => {function BrokenRender(props) {throw new Error(props.label);}ReactNoop.renderToRootWithID(<BrokenRender label="a" />, 'a');await waitForThrow('a');expect(ReactNoop.getChildrenAsJSX('a')).toEqual(null);ReactNoop.renderToRootWithID(<BrokenRender label="a" />, 'a');ReactNoop.renderToRootWithID(<span prop="b:2" />, 'b');await waitForThrow('a');await waitForAll([]);expect(ReactNoop.getChildrenAsJSX('a')).toEqual(null);expect(ReactNoop.getChildrenAsJSX('b')).toEqual(<span prop="b:2" />);ReactNoop.renderToRootWithID(<span prop="a:3" />, 'a');ReactNoop.renderToRootWithID(<BrokenRender label="b" />, 'b');await waitForThrow('b');expect(ReactNoop.getChildrenAsJSX('a')).toEqual(<span prop="a:3" />);expect(ReactNoop.getChildrenAsJSX('b')).toEqual(null);ReactNoop.renderToRootWithID(<span prop="a:4" />, 'a');ReactNoop.renderToRootWithID(<BrokenRender label="b" />, 'b');ReactNoop.renderToRootWithID(<span prop="c:4" />, 'c');await waitForThrow('b');await waitForAll([]);expect(ReactNoop.getChildrenAsJSX('a')).toEqual(<span prop="a:4" />);expect(ReactNoop.getChildrenAsJSX('b')).toEqual(null);expect(ReactNoop.getChildrenAsJSX('c')).toEqual(<span prop="c:4" />);ReactNoop.renderToRootWithID(<span prop="a:5" />, 'a');ReactNoop.renderToRootWithID(<span prop="b:5" />, 'b');ReactNoop.renderToRootWithID(<span prop="c:5" />, 'c');ReactNoop.renderToRootWithID(<span prop="d:5" />, 'd');ReactNoop.renderToRootWithID(<BrokenRender label="e" />, 'e');await waitForThrow('e');await waitForAll([]);expect(ReactNoop.getChildrenAsJSX('a')).toEqual(<span prop="a:5" />);expect(ReactNoop.getChildrenAsJSX('b')).toEqual(<span prop="b:5" />);expect(ReactNoop.getChildrenAsJSX('c')).toEqual(<span prop="c:5" />);expect(ReactNoop.getChildrenAsJSX('d')).toEqual(<span prop="d:5" />);expect(ReactNoop.getChildrenAsJSX('e')).toEqual(null);ReactNoop.renderToRootWithID(<BrokenRender label="a" />, 'a');ReactNoop.renderToRootWithID(<span prop="b:6" />, 'b');ReactNoop.renderToRootWithID(<BrokenRender label="c" />, 'c');ReactNoop.renderToRootWithID(<span prop="d:6" />, 'd');ReactNoop.renderToRootWithID(<BrokenRender label="e" />, 'e');ReactNoop.renderToRootWithID(<span prop="f:6" />, 'f');await waitForThrow('a');await waitForThrow('c');await waitForThrow('e');await waitForAll([]);expect(ReactNoop.getChildrenAsJSX('a')).toEqual(null);expect(ReactNoop.getChildrenAsJSX('b')).toEqual(<span prop="b:6" />);expect(ReactNoop.getChildrenAsJSX('c')).toEqual(null);expect(ReactNoop.getChildrenAsJSX('d')).toEqual(<span prop="d:6" />);expect(ReactNoop.getChildrenAsJSX('e')).toEqual(null);expect(ReactNoop.getChildrenAsJSX('f')).toEqual(<span prop="f:6" />);ReactNoop.unmountRootWithID('a');ReactNoop.unmountRootWithID('b');ReactNoop.unmountRootWithID('c');ReactNoop.unmountRootWithID('d');ReactNoop.unmountRootWithID('e');ReactNoop.unmountRootWithID('f');await waitForAll([]);expect(ReactNoop.getChildrenAsJSX('a')).toEqual(null);expect(ReactNoop.getChildrenAsJSX('b')).toEqual(null);expect(ReactNoop.getChildrenAsJSX('c')).toEqual(null);expect(ReactNoop.getChildrenAsJSX('d')).toEqual(null);expect(ReactNoop.getChildrenAsJSX('e')).toEqual(null);expect(ReactNoop.getChildrenAsJSX('f')).toEqual(null);});// NOTE: When legacy context is removed, it's probably fine to just delete// this test. There's plenty of test coverage of stack unwinding in general// because it's used for new context, suspense, and many other features.// It has to be tested independently for each feature anyway. So although it// doesn't look like it, this test is specific to legacy context.// @gate !disableLegacyContextit('unwinds the context stack correctly on error', async () => {class Provider extends React.Component {static childContextTypes = {message: PropTypes.string};static contextTypes = {message: PropTypes.string};getChildContext() {return {message: (this.context.message || '') + this.props.message,};}render() {return this.props.children;}}function Connector(props, context) {return <span prop={context.message} />;}Connector.contextTypes = {message: PropTypes.string,};function BadRender() {throw new Error('render error');}class Boundary extends React.Component {state = {error: null};componentDidCatch(error) {this.setState({error});}render() {return (<Provider message="b"><Provider message="c"><Provider message="d"><Provider message="e">{!this.state.error && <BadRender />}</Provider></Provider></Provider></Provider>);}}ReactNoop.render(<Provider message="a"><Boundary /><Connector /></Provider>,);await waitForAll([]);// If the context stack does not unwind, span will get 'abcde'expect(ReactNoop).toMatchRenderedOutput(<span prop="a" />);});it('catches reconciler errors in a boundary during mounting', async () => {class ErrorBoundary extends React.Component {state = {error: null};componentDidCatch(error) {this.setState({error});}render() {if (this.state.error) {return <span prop={this.state.error.message} />;}return this.props.children;}}const InvalidType = undefined;function BrokenRender(props) {return <InvalidType />;}ReactNoop.render(<ErrorBoundary><BrokenRender /></ErrorBoundary>,);await expect(async () => await waitForAll([])).toErrorDev(['Warning: React.createElement: type is invalid -- expected a string',// React retries once on error'Warning: React.createElement: type is invalid -- expected a string',]);expect(ReactNoop).toMatchRenderedOutput(<spanprop={'Element type is invalid: expected a string (for built-in components) or ' +'a class/function (for composite components) but got: undefined.' +(__DEV__? " You likely forgot to export your component from the file it's " +'defined in, or you might have mixed up default and named imports.' +'\n\nCheck the render method of `BrokenRender`.'
: '')}/>,);});it('catches reconciler errors in a boundary during update', async () => {class ErrorBoundary extends React.Component {state = {error: null};componentDidCatch(error) {this.setState({error});}render() {if (this.state.error) {return <span prop={this.state.error.message} />;}return this.props.children;}}const InvalidType = undefined;function BrokenRender(props) {return props.fail ? <InvalidType /> : <span />;}ReactNoop.render(<ErrorBoundary><BrokenRender fail={false} /></ErrorBoundary>,);await waitForAll([]);ReactNoop.render(<ErrorBoundary><BrokenRender fail={true} /></ErrorBoundary>,);await expect(async () => await waitForAll([])).toErrorDev(['Warning: React.createElement: type is invalid -- expected a string',// React retries once on error'Warning: React.createElement: type is invalid -- expected a string',]);expect(ReactNoop).toMatchRenderedOutput(<spanprop={'Element type is invalid: expected a string (for built-in components) or ' +'a class/function (for composite components) but got: undefined.' +(__DEV__? " You likely forgot to export your component from the file it's " +'defined in, or you might have mixed up default and named imports.' +'\n\nCheck the render method of `BrokenRender`.'
: '')}/>,);});it('recovers from uncaught reconciler errors', async () => {const InvalidType = undefined;expect(() => ReactNoop.render(<InvalidType />)).toErrorDev('Warning: React.createElement: type is invalid -- expected a string',{withoutStack: true},);await waitForThrow('Element type is invalid: expected a string (for built-in components) or ' +'a class/function (for composite components) but got: undefined.' +(__DEV__? " You likely forgot to export your component from the file it's " +'defined in, or you might have mixed up default and named imports.': ''),);ReactNoop.render(<span prop="hi" />);await waitForAll([]);expect(ReactNoop).toMatchRenderedOutput(<span prop="hi" />);});it('unmounts components with uncaught errors', async () => {let inst;class BrokenRenderAndUnmount extends React.Component {state = {fail: false};componentWillUnmount() {Scheduler.log('BrokenRenderAndUnmount componentWillUnmount');}render() {inst = this;if (this.state.fail) {throw new Error('Hello.');}return null;}}class Parent extends React.Component {componentWillUnmount() {Scheduler.log('Parent componentWillUnmount [!]');throw new Error('One does not simply unmount me.');}render() {return this.props.children;}}ReactNoop.render(<Parent><Parent><BrokenRenderAndUnmount /></Parent></Parent>,);await waitForAll([]);let aggregateError;try {ReactNoop.flushSync(() => {inst.setState({fail: true});});} catch (e) {aggregateError = e;}assertLog([// Attempt to clean up.// Errors in parents shouldn't stop children from unmounting.'Parent componentWillUnmount [!]','Parent componentWillUnmount [!]','BrokenRenderAndUnmount componentWillUnmount',]);expect(ReactNoop).toMatchRenderedOutput(null);// React threw both errors as a single AggregateErrorconst errors = aggregateError.errors;expect(errors.length).toBe(2);expect(errors[0].message).toBe('Hello.');expect(errors[1].message).toBe('One does not simply unmount me.');});it('does not interrupt unmounting if detaching a ref throws', async () => {class Bar extends React.Component {componentWillUnmount() {Scheduler.log('Bar unmount');}render() {return <span prop="Bar" />;}}function barRef(inst) {if (inst === null) {Scheduler.log('barRef detach');throw new Error('Detach error');}Scheduler.log('barRef attach');}function Foo(props) {return <div>{props.hide ? null : <Bar ref={barRef} />}</div>;}ReactNoop.render(<Foo />);await waitForAll(['barRef attach']);expect(ReactNoop).toMatchRenderedOutput(<div><span prop="Bar" /></div>,);// UnmountReactNoop.render(<Foo hide={true} />);await waitForThrow('Detach error');assertLog(['barRef detach',// Bar should unmount even though its ref threw an error while detaching'Bar unmount',]);// Because there was an error, entire tree should unmountexpect(ReactNoop).toMatchRenderedOutput(null);});it('handles error thrown by host config while working on failed root', async () => {ReactNoop.render(<errorInBeginPhase />);await waitForThrow('Error in host config.');});it('handles error thrown by top-level callback', async () => {ReactNoop.render(<div />, () => {throw new Error('Error!');});await waitForThrow('Error!');});it('error boundaries capture non-errors', async () => {spyOnProd(console, 'error').mockImplementation(() => {});spyOnDev(console, 'error').mockImplementation(() => {});class ErrorBoundary extends React.Component {state = {error: null};componentDidCatch(error) {// Should not be calledScheduler.log('componentDidCatch');this.setState({error});}render() {if (this.state.error) {Scheduler.log('ErrorBoundary (catch)');return (<spanprop={`Caught an error: ${this.state.error.nonStandardMessage}`}
/>);}Scheduler.log('ErrorBoundary (try)');return this.props.children;}}function Indirection({children}) {Scheduler.log('Indirection');return children;}const notAnError = {nonStandardMessage: 'oops'};function BadRender({unused}) {Scheduler.log('BadRender');throw notAnError;}ReactNoop.render(<ErrorBoundary><Indirection><BadRender /></Indirection></ErrorBoundary>,);await waitForAll(['ErrorBoundary (try)','Indirection','BadRender',// React retries one more time'ErrorBoundary (try)','Indirection','BadRender',// Errored again on retry. Now handle it.'componentDidCatch','ErrorBoundary (catch)',]);expect(ReactNoop).toMatchRenderedOutput(<span prop="Caught an error: oops" />,);if (__DEV__) {expect(console.error).toHaveBeenCalledTimes(1);expect(console.error.mock.calls[0][0]).toContain('The above error occurred in the <BadRender> component:',);} else {expect(console.error).toHaveBeenCalledTimes(1);expect(console.error.mock.calls[0][0]).toBe(notAnError);}});// TODO: Error boundary does not catch promisesit('continues working on siblings of a component that throws', async () => {class ErrorBoundary extends React.Component {state = {error: null};componentDidCatch(error) {Scheduler.log('componentDidCatch');this.setState({error});}render() {if (this.state.error) {Scheduler.log('ErrorBoundary (catch)');return <ErrorMessage error={this.state.error} />;}Scheduler.log('ErrorBoundary (try)');return this.props.children;}}function ErrorMessage({error}) {Scheduler.log('ErrorMessage');return <span prop={`Caught an error: ${error.message}`} />;
}function BadRenderSibling({unused}) {Scheduler.log('BadRenderSibling');return null;}function BadRender({unused}) {Scheduler.log('throw');throw new Error('oops!');}ReactNoop.render(<ErrorBoundary><BadRender /><BadRenderSibling /><BadRenderSibling /></ErrorBoundary>,);await waitForAll(['ErrorBoundary (try)','throw',// Continue rendering siblings after BadRender throws// React retries one more time'ErrorBoundary (try)','throw',// Errored again on retry. Now handle it.'componentDidCatch','ErrorBoundary (catch)','ErrorMessage',]);expect(ReactNoop).toMatchRenderedOutput(<span prop="Caught an error: oops!" />,);});it('calls the correct lifecycles on the error boundary after catching an error (mixed)', async () => {// This test seems a bit contrived, but it's based on an actual regression// where we checked for the existence of didUpdate instead of didMount, and// didMount was not defined.function BadRender({unused}) {Scheduler.log('throw');throw new Error('oops!');}class Parent extends React.Component {state = {error: null, other: false};componentDidCatch(error) {Scheduler.log('did catch');this.setState({error});}componentDidUpdate() {Scheduler.log('did update');}render() {if (this.state.error) {Scheduler.log('render error message');return <span prop={`Caught an error: ${this.state.error.message}`} />;
}Scheduler.log('render');return <BadRender />;}}ReactNoop.render(<Parent step={1} />);await waitFor(['render','throw','render','throw','did catch','render error message','did update',]);expect(ReactNoop).toMatchRenderedOutput(<span prop="Caught an error: oops!" />,);});it('provides component stack to the error boundary with componentDidCatch', async () => {class ErrorBoundary extends React.Component {state = {error: null, errorInfo: null};componentDidCatch(error, errorInfo) {this.setState({error, errorInfo});}render() {if (this.state.errorInfo) {Scheduler.log('render error message');return (<spanprop={`Caught an error:${normalizeCodeLocInfo(
this.state.errorInfo.componentStack,
)}.`}
/>);}return this.props.children;}}function BrokenRender(props) {throw new Error('Hello');}ReactNoop.render(<ErrorBoundary><BrokenRender /></ErrorBoundary>,);await waitForAll(['render error message']);expect(ReactNoop).toMatchRenderedOutput(<spanprop={'Caught an error:\n' +
' in BrokenRender (at **)\n' +
' in ErrorBoundary (at **).'}/>,);});it('does not provide component stack to the error boundary with getDerivedStateFromError', async () => {class ErrorBoundary extends React.Component {state = {error: null};static getDerivedStateFromError(error, errorInfo) {expect(errorInfo).toBeUndefined();return {error};}render() {if (this.state.error) {return <span prop={`Caught an error: ${this.state.error.message}`} />;
}return this.props.children;}}function BrokenRender(props) {throw new Error('Hello');}ReactNoop.render(<ErrorBoundary><BrokenRender /></ErrorBoundary>,);await waitForAll([]);expect(ReactNoop).toMatchRenderedOutput(<span prop="Caught an error: Hello" />,);});it('provides component stack even if overriding prepareStackTrace', async () => {Error.prepareStackTrace = function (error, callsites) {const stack = ['An error occurred:', error.message];for (let i = 0; i < callsites.length; i++) {const callsite = callsites[i];stack.push('\t' + callsite.getFunctionName(),
'\t\tat ' + callsite.getFileName(),
'\t\ton line ' + callsite.getLineNumber(),
);}return stack.join('\n');
};class ErrorBoundary extends React.Component {state = {error: null, errorInfo: null};componentDidCatch(error, errorInfo) {this.setState({error, errorInfo});}render() {if (this.state.errorInfo) {Scheduler.log('render error message');return (<spanprop={`Caught an error:${normalizeCodeLocInfo(
this.state.errorInfo.componentStack,
)}.`}
/>);}return this.props.children;}}function BrokenRender(props) {throw new Error('Hello');}ReactNoop.render(<ErrorBoundary><BrokenRender /></ErrorBoundary>,);await waitForAll(['render error message']);Error.prepareStackTrace = undefined;expect(ReactNoop).toMatchRenderedOutput(<spanprop={'Caught an error:\n' +
' in BrokenRender (at **)\n' +
' in ErrorBoundary (at **).'}/>,);});// @gate !disableModulePatternComponentsit('handles error thrown inside getDerivedStateFromProps of a module-style context provider', async () => {function Provider() {return {getChildContext() {return {foo: 'bar'};},render() {return 'Hi';},};}Provider.childContextTypes = {x: () => {},};Provider.getDerivedStateFromProps = () => {throw new Error('Oops!');};ReactNoop.render(<Provider />);await expect(async () => {await waitForThrow('Oops!');}).toErrorDev(['Warning: The <Provider /> component appears to be a function component that returns a class instance. ' +'Change Provider to a class that extends React.Component instead. ' +"If you can't use a class try assigning the prototype on the function as a workaround. " +'`Provider.prototype = React.Component.prototype`. ' +
"Don't use an arrow function since it cannot be called with `new` by React.",
]);});it('uncaught errors should be discarded if the render is aborted', async () => {const root = ReactNoop.createRoot();function Oops({unused}) {Scheduler.log('Oops');throw Error('Oops');}await act(async () => {React.startTransition(() => {root.render(<Oops />);});// Render past the component that throws, then yield.await waitFor(['Oops']);expect(root).toMatchRenderedOutput(null);// Interleaved update. When the root completes, instead of throwing the// error, it should try rendering again. This update will cause it to// recover gracefully.React.startTransition(() => {root.render('Everything is fine.');});});// Should finish without throwing.expect(root).toMatchRenderedOutput('Everything is fine.');});it('uncaught errors are discarded if the render is aborted, case 2', async () => {const {useState} = React;const root = ReactNoop.createRoot();let setShouldThrow;function Oops() {const [shouldThrow, _setShouldThrow] = useState(false);setShouldThrow = _setShouldThrow;if (shouldThrow) {throw Error('Oops');}return null;}function AllGood() {Scheduler.log('Everything is fine.');return 'Everything is fine.';}await act(() => {root.render(<Oops />);});await act(async () => {// Schedule a default pri and a low pri update on the root.root.render(<Oops />);React.startTransition(() => {root.render(<AllGood />);});// Render through just the default pri update. The low pri update remains on// the queue.await waitFor(['Everything is fine.']);// Schedule a discrete update on a child that triggers an error.// The root should capture this error. But since there's still a pending// update on the root, the error should be suppressed.ReactNoop.discreteUpdates(() => {setShouldThrow(true);});});// Should render the final state without throwing the error.assertLog(['Everything is fine.']);expect(root).toMatchRenderedOutput('Everything is fine.');});it("does not infinite loop if there's a render phase update in the same render as an error", async () => {// Some React features may schedule a render phase update as an// implementation detail. When an error is accompanied by a render phase// update, we assume that it comes from React internals, because render// phase updates triggered from userspace are not allowed (we log a// warning). So we keep attempting to recover until no more opaque// identifiers need to be upgraded. However, we should give up after some// point to prevent an infinite loop in the case where there is (by// accident) a render phase triggered from userspace.spyOnDev(console, 'error').mockImplementation(() => {});let numberOfThrows = 0;let setStateInRenderPhase;function Child() {const [, setState] = React.useState(0);setStateInRenderPhase = setState;return 'All good';}function App({shouldThrow}) {if (shouldThrow) {setStateInRenderPhase();numberOfThrows++;throw new Error('Oops!');}return <Child />;}const root = ReactNoop.createRoot();await act(() => {root.render(<App shouldThrow={false} />);});expect(root).toMatchRenderedOutput('All good');let error;try {await act(() => {root.render(<App shouldThrow={true} />);});} catch (e) {error = e;}expect(error.message).toBe('Oops!');expect(numberOfThrows < 100).toBe(true);if (__DEV__) {expect(console.error).toHaveBeenCalledTimes(2);expect(console.error.mock.calls[0][0]).toContain('Cannot update a component (`%s`) while rendering a different component',
);expect(console.error.mock.calls[1][0]).toContain('The above error occurred in the <App> component',);}});if (global.__PERSISTENT__) {it('regression test: should fatal if error is thrown at the root', async () => {const root = ReactNoop.createRoot();root.render('Error when completing root');await waitForThrow('Error when completing root');});}});