diff --git a/packages/react-dom/src/__tests__/ReactLegacyErrorBoundaries-test.internal.js b/packages/react-dom/src/__tests__/ReactLegacyErrorBoundaries-test.internal.js
new file mode 100644
index 0000000000000..6bf9fc164f646
--- /dev/null
+++ b/packages/react-dom/src/__tests__/ReactLegacyErrorBoundaries-test.internal.js
@@ -0,0 +1,2125 @@
+/**
+ * Copyright (c) Facebook, Inc. and its 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 PropTypes;
+let React;
+let ReactDOM;
+let ReactFeatureFlags;
+
+// TODO: Refactor this test once componentDidCatch setState is deprecated.
+describe('ReactLegacyErrorBoundaries', () => {
+ let log;
+
+ let BrokenConstructor;
+ let BrokenComponentWillMount;
+ let BrokenComponentDidMount;
+ let BrokenComponentWillReceiveProps;
+ let BrokenComponentWillUpdate;
+ let BrokenComponentDidUpdate;
+ let BrokenComponentWillUnmount;
+ let BrokenRenderErrorBoundary;
+ let BrokenComponentWillMountErrorBoundary;
+ let BrokenComponentDidMountErrorBoundary;
+ let BrokenRender;
+ let ErrorBoundary;
+ let ErrorMessage;
+ let NoopErrorBoundary;
+ let RetryErrorBoundary;
+ let Normal;
+
+ beforeEach(() => {
+ jest.resetModules();
+ PropTypes = require('prop-types');
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
+ ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
+ ReactDOM = require('react-dom');
+ React = require('react');
+
+ log = [];
+
+ BrokenConstructor = class extends React.Component {
+ constructor(props) {
+ super(props);
+ log.push('BrokenConstructor constructor [!]');
+ throw new Error('Hello');
+ }
+ render() {
+ log.push('BrokenConstructor render');
+ return
{this.props.children}
;
+ }
+ UNSAFE_componentWillMount() {
+ log.push('BrokenConstructor componentWillMount');
+ }
+ componentDidMount() {
+ log.push('BrokenConstructor componentDidMount');
+ }
+ UNSAFE_componentWillReceiveProps() {
+ log.push('BrokenConstructor componentWillReceiveProps');
+ }
+ UNSAFE_componentWillUpdate() {
+ log.push('BrokenConstructor componentWillUpdate');
+ }
+ componentDidUpdate() {
+ log.push('BrokenConstructor componentDidUpdate');
+ }
+ componentWillUnmount() {
+ log.push('BrokenConstructor componentWillUnmount');
+ }
+ };
+
+ BrokenComponentWillMount = class extends React.Component {
+ constructor(props) {
+ super(props);
+ log.push('BrokenComponentWillMount constructor');
+ }
+ render() {
+ log.push('BrokenComponentWillMount render');
+ return {this.props.children}
;
+ }
+ UNSAFE_componentWillMount() {
+ log.push('BrokenComponentWillMount componentWillMount [!]');
+ throw new Error('Hello');
+ }
+ componentDidMount() {
+ log.push('BrokenComponentWillMount componentDidMount');
+ }
+ UNSAFE_componentWillReceiveProps() {
+ log.push('BrokenComponentWillMount componentWillReceiveProps');
+ }
+ UNSAFE_componentWillUpdate() {
+ log.push('BrokenComponentWillMount componentWillUpdate');
+ }
+ componentDidUpdate() {
+ log.push('BrokenComponentWillMount componentDidUpdate');
+ }
+ componentWillUnmount() {
+ log.push('BrokenComponentWillMount componentWillUnmount');
+ }
+ };
+
+ BrokenComponentDidMount = class extends React.Component {
+ constructor(props) {
+ super(props);
+ log.push('BrokenComponentDidMount constructor');
+ }
+ render() {
+ log.push('BrokenComponentDidMount render');
+ return {this.props.children}
;
+ }
+ UNSAFE_componentWillMount() {
+ log.push('BrokenComponentDidMount componentWillMount');
+ }
+ componentDidMount() {
+ log.push('BrokenComponentDidMount componentDidMount [!]');
+ throw new Error('Hello');
+ }
+ UNSAFE_componentWillReceiveProps() {
+ log.push('BrokenComponentDidMount componentWillReceiveProps');
+ }
+ UNSAFE_componentWillUpdate() {
+ log.push('BrokenComponentDidMount componentWillUpdate');
+ }
+ componentDidUpdate() {
+ log.push('BrokenComponentDidMount componentDidUpdate');
+ }
+ componentWillUnmount() {
+ log.push('BrokenComponentDidMount componentWillUnmount');
+ }
+ };
+
+ BrokenComponentWillReceiveProps = class extends React.Component {
+ constructor(props) {
+ super(props);
+ log.push('BrokenComponentWillReceiveProps constructor');
+ }
+ render() {
+ log.push('BrokenComponentWillReceiveProps render');
+ return {this.props.children}
;
+ }
+ UNSAFE_componentWillMount() {
+ log.push('BrokenComponentWillReceiveProps componentWillMount');
+ }
+ componentDidMount() {
+ log.push('BrokenComponentWillReceiveProps componentDidMount');
+ }
+ UNSAFE_componentWillReceiveProps() {
+ log.push(
+ 'BrokenComponentWillReceiveProps componentWillReceiveProps [!]',
+ );
+ throw new Error('Hello');
+ }
+ UNSAFE_componentWillUpdate() {
+ log.push('BrokenComponentWillReceiveProps componentWillUpdate');
+ }
+ componentDidUpdate() {
+ log.push('BrokenComponentWillReceiveProps componentDidUpdate');
+ }
+ componentWillUnmount() {
+ log.push('BrokenComponentWillReceiveProps componentWillUnmount');
+ }
+ };
+
+ BrokenComponentWillUpdate = class extends React.Component {
+ constructor(props) {
+ super(props);
+ log.push('BrokenComponentWillUpdate constructor');
+ }
+ render() {
+ log.push('BrokenComponentWillUpdate render');
+ return {this.props.children}
;
+ }
+ UNSAFE_componentWillMount() {
+ log.push('BrokenComponentWillUpdate componentWillMount');
+ }
+ componentDidMount() {
+ log.push('BrokenComponentWillUpdate componentDidMount');
+ }
+ UNSAFE_componentWillReceiveProps() {
+ log.push('BrokenComponentWillUpdate componentWillReceiveProps');
+ }
+ UNSAFE_componentWillUpdate() {
+ log.push('BrokenComponentWillUpdate componentWillUpdate [!]');
+ throw new Error('Hello');
+ }
+ componentDidUpdate() {
+ log.push('BrokenComponentWillUpdate componentDidUpdate');
+ }
+ componentWillUnmount() {
+ log.push('BrokenComponentWillUpdate componentWillUnmount');
+ }
+ };
+
+ BrokenComponentDidUpdate = class extends React.Component {
+ static defaultProps = {
+ errorText: 'Hello',
+ };
+ constructor(props) {
+ super(props);
+ log.push('BrokenComponentDidUpdate constructor');
+ }
+ render() {
+ log.push('BrokenComponentDidUpdate render');
+ return {this.props.children}
;
+ }
+ UNSAFE_componentWillMount() {
+ log.push('BrokenComponentDidUpdate componentWillMount');
+ }
+ componentDidMount() {
+ log.push('BrokenComponentDidUpdate componentDidMount');
+ }
+ UNSAFE_componentWillReceiveProps() {
+ log.push('BrokenComponentDidUpdate componentWillReceiveProps');
+ }
+ UNSAFE_componentWillUpdate() {
+ log.push('BrokenComponentDidUpdate componentWillUpdate');
+ }
+ componentDidUpdate() {
+ log.push('BrokenComponentDidUpdate componentDidUpdate [!]');
+ throw new Error(this.props.errorText);
+ }
+ componentWillUnmount() {
+ log.push('BrokenComponentDidUpdate componentWillUnmount');
+ }
+ };
+
+ BrokenComponentWillUnmount = class extends React.Component {
+ static defaultProps = {
+ errorText: 'Hello',
+ };
+ constructor(props) {
+ super(props);
+ log.push('BrokenComponentWillUnmount constructor');
+ }
+ render() {
+ log.push('BrokenComponentWillUnmount render');
+ return {this.props.children}
;
+ }
+ UNSAFE_componentWillMount() {
+ log.push('BrokenComponentWillUnmount componentWillMount');
+ }
+ componentDidMount() {
+ log.push('BrokenComponentWillUnmount componentDidMount');
+ }
+ UNSAFE_componentWillReceiveProps() {
+ log.push('BrokenComponentWillUnmount componentWillReceiveProps');
+ }
+ UNSAFE_componentWillUpdate() {
+ log.push('BrokenComponentWillUnmount componentWillUpdate');
+ }
+ componentDidUpdate() {
+ log.push('BrokenComponentWillUnmount componentDidUpdate');
+ }
+ componentWillUnmount() {
+ log.push('BrokenComponentWillUnmount componentWillUnmount [!]');
+ throw new Error(this.props.errorText);
+ }
+ };
+
+ BrokenComponentWillMountErrorBoundary = class extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {error: null};
+ log.push('BrokenComponentWillMountErrorBoundary constructor');
+ }
+ render() {
+ if (this.state.error) {
+ log.push('BrokenComponentWillMountErrorBoundary render error');
+ return Caught an error: {this.state.error.message}.
;
+ }
+ log.push('BrokenComponentWillMountErrorBoundary render success');
+ return {this.props.children}
;
+ }
+ UNSAFE_componentWillMount() {
+ log.push(
+ 'BrokenComponentWillMountErrorBoundary componentWillMount [!]',
+ );
+ throw new Error('Hello');
+ }
+ componentDidMount() {
+ log.push('BrokenComponentWillMountErrorBoundary componentDidMount');
+ }
+ componentWillUnmount() {
+ log.push('BrokenComponentWillMountErrorBoundary componentWillUnmount');
+ }
+ componentDidCatch(error) {
+ log.push('BrokenComponentWillMountErrorBoundary componentDidCatch');
+ this.setState({error});
+ }
+ };
+
+ BrokenComponentDidMountErrorBoundary = class extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {error: null};
+ log.push('BrokenComponentDidMountErrorBoundary constructor');
+ }
+ render() {
+ if (this.state.error) {
+ log.push('BrokenComponentDidMountErrorBoundary render error');
+ return Caught an error: {this.state.error.message}.
;
+ }
+ log.push('BrokenComponentDidMountErrorBoundary render success');
+ return {this.props.children}
;
+ }
+ UNSAFE_componentWillMount() {
+ log.push('BrokenComponentDidMountErrorBoundary componentWillMount');
+ }
+ componentDidMount() {
+ log.push('BrokenComponentDidMountErrorBoundary componentDidMount [!]');
+ throw new Error('Hello');
+ }
+ componentWillUnmount() {
+ log.push('BrokenComponentDidMountErrorBoundary componentWillUnmount');
+ }
+ componentDidCatch(error) {
+ log.push('BrokenComponentDidMountErrorBoundary componentDidCatch');
+ this.setState({error});
+ }
+ };
+
+ BrokenRenderErrorBoundary = class extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {error: null};
+ log.push('BrokenRenderErrorBoundary constructor');
+ }
+ render() {
+ if (this.state.error) {
+ log.push('BrokenRenderErrorBoundary render error [!]');
+ throw new Error('Hello');
+ }
+ log.push('BrokenRenderErrorBoundary render success');
+ return {this.props.children}
;
+ }
+ UNSAFE_componentWillMount() {
+ log.push('BrokenRenderErrorBoundary componentWillMount');
+ }
+ componentDidMount() {
+ log.push('BrokenRenderErrorBoundary componentDidMount');
+ }
+ componentWillUnmount() {
+ log.push('BrokenRenderErrorBoundary componentWillUnmount');
+ }
+ componentDidCatch(error) {
+ log.push('BrokenRenderErrorBoundary componentDidCatch');
+ this.setState({error});
+ }
+ };
+
+ BrokenRender = class extends React.Component {
+ constructor(props) {
+ super(props);
+ log.push('BrokenRender constructor');
+ }
+ render() {
+ log.push('BrokenRender render [!]');
+ throw new Error('Hello');
+ }
+ UNSAFE_componentWillMount() {
+ log.push('BrokenRender componentWillMount');
+ }
+ componentDidMount() {
+ log.push('BrokenRender componentDidMount');
+ }
+ UNSAFE_componentWillReceiveProps() {
+ log.push('BrokenRender componentWillReceiveProps');
+ }
+ UNSAFE_componentWillUpdate() {
+ log.push('BrokenRender componentWillUpdate');
+ }
+ componentDidUpdate() {
+ log.push('BrokenRender componentDidUpdate');
+ }
+ componentWillUnmount() {
+ log.push('BrokenRender componentWillUnmount');
+ }
+ };
+
+ NoopErrorBoundary = class extends React.Component {
+ constructor(props) {
+ super(props);
+ log.push('NoopErrorBoundary constructor');
+ }
+ render() {
+ log.push('NoopErrorBoundary render');
+ return ;
+ }
+ UNSAFE_componentWillMount() {
+ log.push('NoopErrorBoundary componentWillMount');
+ }
+ componentDidMount() {
+ log.push('NoopErrorBoundary componentDidMount');
+ }
+ componentWillUnmount() {
+ log.push('NoopErrorBoundary componentWillUnmount');
+ }
+ componentDidCatch() {
+ log.push('NoopErrorBoundary componentDidCatch');
+ }
+ };
+
+ Normal = class extends React.Component {
+ static defaultProps = {
+ logName: 'Normal',
+ };
+ constructor(props) {
+ super(props);
+ log.push(`${this.props.logName} constructor`);
+ }
+ render() {
+ log.push(`${this.props.logName} render`);
+ return {this.props.children}
;
+ }
+ UNSAFE_componentWillMount() {
+ log.push(`${this.props.logName} componentWillMount`);
+ }
+ componentDidMount() {
+ log.push(`${this.props.logName} componentDidMount`);
+ }
+ UNSAFE_componentWillReceiveProps() {
+ log.push(`${this.props.logName} componentWillReceiveProps`);
+ }
+ UNSAFE_componentWillUpdate() {
+ log.push(`${this.props.logName} componentWillUpdate`);
+ }
+ componentDidUpdate() {
+ log.push(`${this.props.logName} componentDidUpdate`);
+ }
+ componentWillUnmount() {
+ log.push(`${this.props.logName} componentWillUnmount`);
+ }
+ };
+
+ ErrorBoundary = class extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {error: null};
+ log.push(`${this.props.logName} constructor`);
+ }
+ render() {
+ if (this.state.error && !this.props.forceRetry) {
+ log.push(`${this.props.logName} render error`);
+ return this.props.renderError(this.state.error, this.props);
+ }
+ log.push(`${this.props.logName} render success`);
+ return {this.props.children}
;
+ }
+ componentDidCatch(error) {
+ log.push(`${this.props.logName} componentDidCatch`);
+ this.setState({error});
+ }
+ UNSAFE_componentWillMount() {
+ log.push(`${this.props.logName} componentWillMount`);
+ }
+ componentDidMount() {
+ log.push(`${this.props.logName} componentDidMount`);
+ }
+ UNSAFE_componentWillReceiveProps() {
+ log.push(`${this.props.logName} componentWillReceiveProps`);
+ }
+ UNSAFE_componentWillUpdate() {
+ log.push(`${this.props.logName} componentWillUpdate`);
+ }
+ componentDidUpdate() {
+ log.push(`${this.props.logName} componentDidUpdate`);
+ }
+ componentWillUnmount() {
+ log.push(`${this.props.logName} componentWillUnmount`);
+ }
+ };
+ ErrorBoundary.defaultProps = {
+ logName: 'ErrorBoundary',
+ renderError(error, props) {
+ return (
+
+ Caught an error: {error.message}.
+
+ );
+ },
+ };
+
+ RetryErrorBoundary = class extends React.Component {
+ constructor(props) {
+ super(props);
+ log.push('RetryErrorBoundary constructor');
+ }
+ render() {
+ log.push('RetryErrorBoundary render');
+ return ;
+ }
+ UNSAFE_componentWillMount() {
+ log.push('RetryErrorBoundary componentWillMount');
+ }
+ componentDidMount() {
+ log.push('RetryErrorBoundary componentDidMount');
+ }
+ componentWillUnmount() {
+ log.push('RetryErrorBoundary componentWillUnmount');
+ }
+ componentDidCatch(error) {
+ log.push('RetryErrorBoundary componentDidCatch [!]');
+ // In Fiber, calling setState() (and failing) is treated as a rethrow.
+ this.setState({});
+ }
+ };
+
+ ErrorMessage = class extends React.Component {
+ constructor(props) {
+ super(props);
+ log.push('ErrorMessage constructor');
+ }
+ UNSAFE_componentWillMount() {
+ log.push('ErrorMessage componentWillMount');
+ }
+ componentDidMount() {
+ log.push('ErrorMessage componentDidMount');
+ }
+ componentWillUnmount() {
+ log.push('ErrorMessage componentWillUnmount');
+ }
+ render() {
+ log.push('ErrorMessage render');
+ return Caught an error: {this.props.message}.
;
+ }
+ };
+ });
+
+ it('does not swallow exceptions on mounting without boundaries', () => {
+ let container = document.createElement('div');
+ expect(() => {
+ ReactDOM.render(, container);
+ }).toThrow('Hello');
+
+ container = document.createElement('div');
+ expect(() => {
+ ReactDOM.render(, container);
+ }).toThrow('Hello');
+
+ container = document.createElement('div');
+ expect(() => {
+ ReactDOM.render(, container);
+ }).toThrow('Hello');
+ });
+
+ it('does not swallow exceptions on updating without boundaries', () => {
+ let container = document.createElement('div');
+ ReactDOM.render(, container);
+ expect(() => {
+ ReactDOM.render(, container);
+ }).toThrow('Hello');
+
+ container = document.createElement('div');
+ ReactDOM.render(, container);
+ expect(() => {
+ ReactDOM.render(, container);
+ }).toThrow('Hello');
+
+ container = document.createElement('div');
+ ReactDOM.render(, container);
+ expect(() => {
+ ReactDOM.render(, container);
+ }).toThrow('Hello');
+ });
+
+ it('does not swallow exceptions on unmounting without boundaries', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(, container);
+ expect(() => {
+ ReactDOM.unmountComponentAtNode(container);
+ }).toThrow('Hello');
+ });
+
+ it('prevents errors from leaking into other roots', () => {
+ const container1 = document.createElement('div');
+ const container2 = document.createElement('div');
+ const container3 = document.createElement('div');
+
+ ReactDOM.render(Before 1, container1);
+ expect(() => {
+ ReactDOM.render(, container2);
+ }).toThrow('Hello');
+ ReactDOM.render(
+
+
+ ,
+ container3,
+ );
+ expect(container1.firstChild.textContent).toBe('Before 1');
+ expect(container2.firstChild).toBe(null);
+ expect(container3.firstChild.textContent).toBe('Caught an error: Hello.');
+
+ ReactDOM.render(After 1, container1);
+ ReactDOM.render(After 2, container2);
+ ReactDOM.render(
+ After 3,
+ container3,
+ );
+ expect(container1.firstChild.textContent).toBe('After 1');
+ expect(container2.firstChild.textContent).toBe('After 2');
+ expect(container3.firstChild.textContent).toBe('After 3');
+
+ ReactDOM.unmountComponentAtNode(container1);
+ ReactDOM.unmountComponentAtNode(container2);
+ ReactDOM.unmountComponentAtNode(container3);
+ expect(container1.firstChild).toBe(null);
+ expect(container2.firstChild).toBe(null);
+ expect(container3.firstChild).toBe(null);
+ });
+
+ it('renders an error state if child throws in render', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ // Fiber mounts with null children before capturing error
+ 'ErrorBoundary componentDidMount',
+ // Catch and render an error message
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('renders an error state if child throws in constructor', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'BrokenConstructor constructor [!]',
+ // Fiber mounts with null children before capturing error
+ 'ErrorBoundary componentDidMount',
+ // Catch and render an error message
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('renders an error state if child throws in componentWillMount', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'BrokenComponentWillMount constructor',
+ 'BrokenComponentWillMount componentWillMount [!]',
+ 'ErrorBoundary componentDidMount',
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('renders an error state if context provider throws in componentWillMount', () => {
+ class BrokenComponentWillMountWithContext extends React.Component {
+ static childContextTypes = {foo: PropTypes.number};
+ getChildContext() {
+ return {foo: 42};
+ }
+ render() {
+ return {this.props.children}
;
+ }
+ UNSAFE_componentWillMount() {
+ throw new Error('Hello');
+ }
+ }
+
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
+ });
+
+ it('renders an error state if module-style context provider throws in componentWillMount', () => {
+ function BrokenComponentWillMountWithContext() {
+ return {
+ getChildContext() {
+ return {foo: 42};
+ },
+ render() {
+ return {this.props.children}
;
+ },
+ UNSAFE_componentWillMount() {
+ throw new Error('Hello');
+ },
+ };
+ }
+ BrokenComponentWillMountWithContext.childContextTypes = {
+ foo: PropTypes.number,
+ };
+
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
+ });
+
+ it('mounts the error message if mounting fails', () => {
+ function renderError(error) {
+ return ;
+ }
+
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ 'ErrorBoundary componentDidMount',
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorMessage constructor',
+ 'ErrorMessage componentWillMount',
+ 'ErrorMessage render',
+ 'ErrorMessage componentDidMount',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ 'ErrorMessage componentWillUnmount',
+ ]);
+ });
+
+ it('propagates errors on retry on mounting', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+
+ ,
+ container,
+ );
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'RetryErrorBoundary constructor',
+ 'RetryErrorBoundary componentWillMount',
+ 'RetryErrorBoundary render',
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ // In Fiber, failed error boundaries render null before attempting to recover
+ 'RetryErrorBoundary componentDidMount',
+ 'RetryErrorBoundary componentDidCatch [!]',
+ 'ErrorBoundary componentDidMount',
+ // Retry
+ 'RetryErrorBoundary render',
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ // This time, the error propagates to the higher boundary
+ 'RetryErrorBoundary componentWillUnmount',
+ 'ErrorBoundary componentDidCatch',
+ // Render the error
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('propagates errors inside boundary during componentWillMount', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'BrokenComponentWillMountErrorBoundary constructor',
+ 'BrokenComponentWillMountErrorBoundary componentWillMount [!]',
+ // The error propagates to the higher boundary
+ 'ErrorBoundary componentDidMount',
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('propagates errors inside boundary while rendering error state', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+
+ ,
+ container,
+ );
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'BrokenRenderErrorBoundary constructor',
+ 'BrokenRenderErrorBoundary componentWillMount',
+ 'BrokenRenderErrorBoundary render success',
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ // The first error boundary catches the error
+ // It adjusts state but throws displaying the message
+ // Finish mounting with null children
+ 'BrokenRenderErrorBoundary componentDidMount',
+ // Attempt to handle the error
+ 'BrokenRenderErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentDidMount',
+ 'BrokenRenderErrorBoundary render error [!]',
+ // Boundary fails with new error, propagate to next boundary
+ 'BrokenRenderErrorBoundary componentWillUnmount',
+ // Attempt to handle the error again
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('does not call componentWillUnmount when aborting initial mount', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+
+ ,
+ container,
+ );
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ // Render first child
+ 'Normal constructor',
+ 'Normal componentWillMount',
+ 'Normal render',
+ // Render second child (it throws)
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ // Render third child, even though an earlier sibling threw.
+ 'Normal constructor',
+ 'Normal componentWillMount',
+ 'Normal render',
+ // Finish mounting with null children
+ 'ErrorBoundary componentDidMount',
+ // Handle the error
+ 'ErrorBoundary componentDidCatch',
+ // Render the error message
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('resets callback refs if mounting aborts', () => {
+ function childRef(x) {
+ log.push('Child ref is set to ' + x);
+ }
+ function errorMessageRef(x) {
+ log.push('Error message ref is set to ' + x);
+ }
+
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ // Handle error:
+ // Finish mounting with null children
+ 'ErrorBoundary componentDidMount',
+ // Handle the error
+ 'ErrorBoundary componentDidCatch',
+ // Render the error message
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'Error message ref is set to [object HTMLDivElement]',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ 'Error message ref is set to null',
+ ]);
+ });
+
+ it('resets object refs if mounting aborts', () => {
+ let childRef = React.createRef();
+ let errorMessageRef = React.createRef();
+
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ // Handle error:
+ // Finish mounting with null children
+ 'ErrorBoundary componentDidMount',
+ // Handle the error
+ 'ErrorBoundary componentDidCatch',
+ // Render the error message
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+ expect(errorMessageRef.current.toString()).toEqual(
+ '[object HTMLDivElement]',
+ );
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ expect(errorMessageRef.current).toEqual(null);
+ });
+
+ it('successfully mounts if no error occurs', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+ Mounted successfully.
+ ,
+ container,
+ );
+ expect(container.firstChild.textContent).toBe('Mounted successfully.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'ErrorBoundary componentDidMount',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('catches if child throws in constructor during update', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+
+ log.length = 0;
+ ReactDOM.render(
+
+
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillReceiveProps',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render success',
+ 'Normal componentWillReceiveProps',
+ 'Normal componentWillUpdate',
+ 'Normal render',
+ // Normal2 will attempt to mount:
+ 'Normal2 constructor',
+ 'Normal2 componentWillMount',
+ 'Normal2 render',
+ // BrokenConstructor will abort rendering:
+ 'BrokenConstructor constructor [!]',
+ // Finish updating with null children
+ 'Normal componentWillUnmount',
+ 'ErrorBoundary componentDidUpdate',
+ // Handle the error
+ 'ErrorBoundary componentDidCatch',
+ // Render the error message
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('catches if child throws in componentWillMount during update', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+
+ log.length = 0;
+ ReactDOM.render(
+
+
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillReceiveProps',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render success',
+ 'Normal componentWillReceiveProps',
+ 'Normal componentWillUpdate',
+ 'Normal render',
+ // Normal2 will attempt to mount:
+ 'Normal2 constructor',
+ 'Normal2 componentWillMount',
+ 'Normal2 render',
+ // BrokenComponentWillMount will abort rendering:
+ 'BrokenComponentWillMount constructor',
+ 'BrokenComponentWillMount componentWillMount [!]',
+ // Finish updating with null children
+ 'Normal componentWillUnmount',
+ 'ErrorBoundary componentDidUpdate',
+ // Handle the error
+ 'ErrorBoundary componentDidCatch',
+ // Render the error message
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('catches if child throws in componentWillReceiveProps during update', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+ ,
+ container,
+ );
+
+ log.length = 0;
+ ReactDOM.render(
+
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillReceiveProps',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render success',
+ 'Normal componentWillReceiveProps',
+ 'Normal componentWillUpdate',
+ 'Normal render',
+ // BrokenComponentWillReceiveProps will abort rendering:
+ 'BrokenComponentWillReceiveProps componentWillReceiveProps [!]',
+ // Finish updating with null children
+ 'Normal componentWillUnmount',
+ 'BrokenComponentWillReceiveProps componentWillUnmount',
+ 'ErrorBoundary componentDidUpdate',
+ // Handle the error
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('catches if child throws in componentWillUpdate during update', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+ ,
+ container,
+ );
+
+ log.length = 0;
+ ReactDOM.render(
+
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillReceiveProps',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render success',
+ 'Normal componentWillReceiveProps',
+ 'Normal componentWillUpdate',
+ 'Normal render',
+ // BrokenComponentWillUpdate will abort rendering:
+ 'BrokenComponentWillUpdate componentWillReceiveProps',
+ 'BrokenComponentWillUpdate componentWillUpdate [!]',
+ // Finish updating with null children
+ 'Normal componentWillUnmount',
+ 'BrokenComponentWillUpdate componentWillUnmount',
+ 'ErrorBoundary componentDidUpdate',
+ // Handle the error
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('catches if child throws in render during update', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+
+ log.length = 0;
+ ReactDOM.render(
+
+
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillReceiveProps',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render success',
+ 'Normal componentWillReceiveProps',
+ 'Normal componentWillUpdate',
+ 'Normal render',
+ // Normal2 will attempt to mount:
+ 'Normal2 constructor',
+ 'Normal2 componentWillMount',
+ 'Normal2 render',
+ // BrokenRender will abort rendering:
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ // Finish updating with null children
+ 'Normal componentWillUnmount',
+ 'ErrorBoundary componentDidUpdate',
+ // Handle the error
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('keeps refs up-to-date during updates', () => {
+ function child1Ref(x) {
+ log.push('Child1 ref is set to ' + x);
+ }
+ function child2Ref(x) {
+ log.push('Child2 ref is set to ' + x);
+ }
+ function errorMessageRef(x) {
+ log.push('Error message ref is set to ' + x);
+ }
+
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'Child1 ref is set to [object HTMLDivElement]',
+ 'ErrorBoundary componentDidMount',
+ ]);
+
+ log.length = 0;
+ ReactDOM.render(
+
+
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillReceiveProps',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render success',
+ // BrokenRender will abort rendering:
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ // Finish updating with null children
+ 'Child1 ref is set to null',
+ 'ErrorBoundary componentDidUpdate',
+ // Handle the error
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'Error message ref is set to [object HTMLDivElement]',
+ // Child2 ref is never set because its mounting aborted
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ 'Error message ref is set to null',
+ ]);
+ });
+
+ it('recovers from componentWillUnmount errors on update', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+
+ ,
+ container,
+ );
+
+ log.length = 0;
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillReceiveProps',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render success',
+ // Update existing child:
+ 'BrokenComponentWillUnmount componentWillReceiveProps',
+ 'BrokenComponentWillUnmount componentWillUpdate',
+ 'BrokenComponentWillUnmount render',
+ // Unmounting throws:
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
+ // Fiber proceeds with lifecycles despite errors
+ 'Normal componentWillUnmount',
+ // The components have updated in this phase
+ 'BrokenComponentWillUnmount componentDidUpdate',
+ 'ErrorBoundary componentDidUpdate',
+ // Now that commit phase is done, Fiber unmounts the boundary's children
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
+ 'ErrorBoundary componentDidCatch',
+ // The initial render was aborted, so
+ // Fiber retries from the root.
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary componentDidUpdate',
+ // The second willUnmount error should be captured and logged, too.
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ // Render an error now (stack will do it later)
+ 'ErrorBoundary render error',
+ // Attempt to unmount previous child:
+ // Done
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('recovers from nested componentWillUnmount errors on update', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+
+
+ ,
+ container,
+ );
+
+ log.length = 0;
+ ReactDOM.render(
+
+
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillReceiveProps',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render success',
+ // Update existing children:
+ 'Normal componentWillReceiveProps',
+ 'Normal componentWillUpdate',
+ 'Normal render',
+ 'BrokenComponentWillUnmount componentWillReceiveProps',
+ 'BrokenComponentWillUnmount componentWillUpdate',
+ 'BrokenComponentWillUnmount render',
+ // Unmounting throws:
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
+ // Fiber proceeds with lifecycles despite errors
+ 'BrokenComponentWillUnmount componentDidUpdate',
+ 'Normal componentDidUpdate',
+ 'ErrorBoundary componentDidUpdate',
+ 'Normal componentWillUnmount',
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
+ // Now that commit phase is done, Fiber handles errors
+ 'ErrorBoundary componentDidCatch',
+ // The initial render was aborted, so
+ // Fiber retries from the root.
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary componentDidUpdate',
+ // The second willUnmount error should be captured and logged, too.
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ // Render an error now (stack will do it later)
+ 'ErrorBoundary render error',
+ // Done
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('picks the right boundary when handling unmounting errors', () => {
+ function renderInnerError(error) {
+ return Caught an inner error: {error.message}.
;
+ }
+ function renderOuterError(error) {
+ return Caught an outer error: {error.message}.
;
+ }
+
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+
+ ,
+ container,
+ );
+
+ log.length = 0;
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).toBe('Caught an inner error: Hello.');
+ expect(log).toEqual([
+ // Update outer boundary
+ 'OuterErrorBoundary componentWillReceiveProps',
+ 'OuterErrorBoundary componentWillUpdate',
+ 'OuterErrorBoundary render success',
+ // Update inner boundary
+ 'InnerErrorBoundary componentWillReceiveProps',
+ 'InnerErrorBoundary componentWillUpdate',
+ 'InnerErrorBoundary render success',
+ // Try unmounting child
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
+ // Fiber proceeds with lifecycles despite errors
+ // Inner and outer boundaries have updated in this phase
+ 'InnerErrorBoundary componentDidUpdate',
+ 'OuterErrorBoundary componentDidUpdate',
+ // Now that commit phase is done, Fiber handles errors
+ // Only inner boundary receives the error:
+ 'InnerErrorBoundary componentDidCatch',
+ 'InnerErrorBoundary componentWillUpdate',
+ // Render an error now
+ 'InnerErrorBoundary render error',
+ // In Fiber, this was a local update to the
+ // inner boundary so only its hook fires
+ 'InnerErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'OuterErrorBoundary componentWillUnmount',
+ 'InnerErrorBoundary componentWillUnmount',
+ ]);
+ });
+
+ it('can recover from error state', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ // Error boundary doesn't retry by itself:
+ expect(container.textContent).toBe('Caught an error: Hello.');
+
+ // Force the success path:
+ log.length = 0;
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).not.toContain('Caught an error');
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillReceiveProps',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render success',
+ // Mount children:
+ 'Normal constructor',
+ 'Normal componentWillMount',
+ 'Normal render',
+ // Finalize updates:
+ 'Normal componentDidMount',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ 'Normal componentWillUnmount',
+ ]);
+ });
+
+ it('can update multiple times in error state', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+
+ ReactDOM.render(Other screen
, container);
+ expect(container.textContent).toBe('Other screen');
+
+ ReactDOM.unmountComponentAtNode(container);
+ });
+
+ it("doesn't get into inconsistent state during removals", () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+
+ ,
+ container,
+ );
+
+ ReactDOM.render(, container);
+ expect(container.textContent).toBe('Caught an error: Hello.');
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it("doesn't get into inconsistent state during additions", () => {
+ const container = document.createElement('div');
+ ReactDOM.render(, container);
+ ReactDOM.render(
+
+
+
+
+ ,
+ container,
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it("doesn't get into inconsistent state during reorders", () => {
+ function getAMixOfNormalAndBrokenRenderElements() {
+ const elements = [];
+ for (let i = 0; i < 100; i++) {
+ elements.push();
+ }
+ elements.push();
+
+ let currentIndex = elements.length;
+ while (0 !== currentIndex) {
+ const randomIndex = Math.floor(Math.random() * currentIndex);
+ currentIndex -= 1;
+ const temporaryValue = elements[currentIndex];
+ elements[currentIndex] = elements[randomIndex];
+ elements[randomIndex] = temporaryValue;
+ }
+ return elements;
+ }
+
+ class MaybeBrokenRender extends React.Component {
+ render() {
+ if (fail) {
+ throw new Error('Hello');
+ }
+ return {this.props.children}
;
+ }
+ }
+
+ let fail = false;
+ const container = document.createElement('div');
+ ReactDOM.render(
+ {getAMixOfNormalAndBrokenRenderElements()},
+ container,
+ );
+ expect(container.textContent).not.toContain('Caught an error');
+
+ fail = true;
+ ReactDOM.render(
+ {getAMixOfNormalAndBrokenRenderElements()},
+ container,
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('catches errors originating downstream', () => {
+ let fail = false;
+ class Stateful extends React.Component {
+ state = {shouldThrow: false};
+
+ render() {
+ if (fail) {
+ log.push('Stateful render [!]');
+ throw new Error('Hello');
+ }
+ return {this.props.children}
;
+ }
+ }
+
+ let statefulInst;
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+ (statefulInst = inst)} />
+ ,
+ container,
+ );
+
+ log.length = 0;
+ expect(() => {
+ fail = true;
+ statefulInst.forceUpdate();
+ }).not.toThrow();
+
+ expect(log).toEqual([
+ 'Stateful render [!]',
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('catches errors in componentDidMount', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+
+
+
+ ,
+ container,
+ );
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'BrokenComponentWillUnmount constructor',
+ 'BrokenComponentWillUnmount componentWillMount',
+ 'BrokenComponentWillUnmount render',
+ 'Normal constructor',
+ 'Normal componentWillMount',
+ 'Normal render',
+ 'BrokenComponentDidMount constructor',
+ 'BrokenComponentDidMount componentWillMount',
+ 'BrokenComponentDidMount render',
+ 'LastChild constructor',
+ 'LastChild componentWillMount',
+ 'LastChild render',
+ // Start flushing didMount queue
+ 'Normal componentDidMount',
+ 'BrokenComponentWillUnmount componentDidMount',
+ 'BrokenComponentDidMount componentDidMount [!]',
+ // Continue despite the error
+ 'LastChild componentDidMount',
+ 'ErrorBoundary componentDidMount',
+ // Now we are ready to handle the error
+ // Safely unmount every child
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
+ // Continue unmounting safely despite any errors
+ 'Normal componentWillUnmount',
+ 'BrokenComponentDidMount componentWillUnmount',
+ 'LastChild componentWillUnmount',
+ // Handle the error
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ // The willUnmount error should be captured and logged, too.
+ 'ErrorBoundary componentDidUpdate',
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ // The update has finished
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('catches errors in componentDidUpdate', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+
+ log.length = 0;
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillReceiveProps',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render success',
+ 'BrokenComponentDidUpdate componentWillReceiveProps',
+ 'BrokenComponentDidUpdate componentWillUpdate',
+ 'BrokenComponentDidUpdate render',
+ // All lifecycles run
+ 'BrokenComponentDidUpdate componentDidUpdate [!]',
+ 'ErrorBoundary componentDidUpdate',
+ 'BrokenComponentDidUpdate componentWillUnmount',
+ // Then, error is handled
+ 'ErrorBoundary componentDidCatch',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('propagates errors inside boundary during componentDidMount', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+ (
+ We should never catch our own error: {error.message}.
+ )}
+ />
+ ,
+ container,
+ );
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'BrokenComponentDidMountErrorBoundary constructor',
+ 'BrokenComponentDidMountErrorBoundary componentWillMount',
+ 'BrokenComponentDidMountErrorBoundary render success',
+ 'BrokenComponentDidMountErrorBoundary componentDidMount [!]',
+ // Fiber proceeds with the hooks
+ 'ErrorBoundary componentDidMount',
+ 'BrokenComponentDidMountErrorBoundary componentWillUnmount',
+ // The error propagates to the higher boundary
+ 'ErrorBoundary componentDidCatch',
+ // Fiber retries from the root
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
+ });
+
+ it('calls componentDidCatch for each error that is captured', () => {
+ function renderUnmountError(error) {
+ return Caught an unmounting error: {error.message}.
;
+ }
+ function renderUpdateError(error) {
+ return Caught an updating error: {error.message}.
;
+ }
+
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+
+
+
+
+
+
+ ,
+ container,
+ );
+
+ log.length = 0;
+ ReactDOM.render(
+
+
+
+
+
+
+ ,
+ container,
+ );
+
+ expect(container.firstChild.textContent).toBe(
+ 'Caught an unmounting error: E2.' + 'Caught an updating error: E4.',
+ );
+ expect(log).toEqual([
+ // Begin update phase
+ 'OuterErrorBoundary componentWillReceiveProps',
+ 'OuterErrorBoundary componentWillUpdate',
+ 'OuterErrorBoundary render success',
+ 'InnerUnmountBoundary componentWillReceiveProps',
+ 'InnerUnmountBoundary componentWillUpdate',
+ 'InnerUnmountBoundary render success',
+ 'InnerUpdateBoundary componentWillReceiveProps',
+ 'InnerUpdateBoundary componentWillUpdate',
+ 'InnerUpdateBoundary render success',
+ // First come the updates
+ 'BrokenComponentDidUpdate componentWillReceiveProps',
+ 'BrokenComponentDidUpdate componentWillUpdate',
+ 'BrokenComponentDidUpdate render',
+ 'BrokenComponentDidUpdate componentWillReceiveProps',
+ 'BrokenComponentDidUpdate componentWillUpdate',
+ 'BrokenComponentDidUpdate render',
+ // We're in commit phase now, deleting
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
+ // Continue despite errors, handle them after commit is done
+ 'InnerUnmountBoundary componentDidUpdate',
+ // We're still in commit phase, now calling update lifecycles
+ 'BrokenComponentDidUpdate componentDidUpdate [!]',
+ // Again, continue despite errors, we'll handle them later
+ 'BrokenComponentDidUpdate componentDidUpdate [!]',
+ 'InnerUpdateBoundary componentDidUpdate',
+ 'OuterErrorBoundary componentDidUpdate',
+ // After the commit phase, attempt to recover from any errors that
+ // were captured
+ 'BrokenComponentDidUpdate componentWillUnmount',
+ 'BrokenComponentDidUpdate componentWillUnmount',
+ 'InnerUnmountBoundary componentDidCatch',
+ 'InnerUnmountBoundary componentDidCatch',
+ 'InnerUpdateBoundary componentDidCatch',
+ 'InnerUpdateBoundary componentDidCatch',
+ 'InnerUnmountBoundary componentWillUpdate',
+ 'InnerUnmountBoundary render error',
+ 'InnerUpdateBoundary componentWillUpdate',
+ 'InnerUpdateBoundary render error',
+ 'InnerUnmountBoundary componentDidUpdate',
+ 'InnerUpdateBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'OuterErrorBoundary componentWillUnmount',
+ 'InnerUnmountBoundary componentWillUnmount',
+ 'InnerUpdateBoundary componentWillUnmount',
+ ]);
+ });
+
+ it('discards a bad root if the root component fails', () => {
+ const X = null;
+ const Y = undefined;
+ let err1;
+ let err2;
+
+ try {
+ let container = document.createElement('div');
+ expect(() => ReactDOM.render(, container)).toWarnDev(
+ 'React.createElement: type is invalid -- expected a string ' +
+ '(for built-in components) or a class/function ' +
+ '(for composite components) but got: null.',
+ );
+ } catch (err) {
+ err1 = err;
+ }
+ try {
+ let container = document.createElement('div');
+ expect(() => ReactDOM.render(, container)).toWarnDev(
+ 'React.createElement: type is invalid -- expected a string ' +
+ '(for built-in components) or a class/function ' +
+ '(for composite components) but got: undefined.',
+ );
+ } catch (err) {
+ err2 = err;
+ }
+
+ expect(err1.message).toMatch(/got: null/);
+ expect(err2.message).toMatch(/got: undefined/);
+ });
+
+ it('renders empty output if error boundary does not handle the error', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+
+ Sibling
+
+
+
+
,
+ container,
+ );
+ expect(container.firstChild.textContent).toBe('Sibling');
+ expect(log).toEqual([
+ 'NoopErrorBoundary constructor',
+ 'NoopErrorBoundary componentWillMount',
+ 'NoopErrorBoundary render',
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ // In Fiber, noop error boundaries render null
+ 'NoopErrorBoundary componentDidMount',
+ 'NoopErrorBoundary componentDidCatch',
+ // Nothing happens.
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual(['NoopErrorBoundary componentWillUnmount']);
+ });
+
+ it('passes first error when two errors happen in commit', () => {
+ const errors = [];
+ let caughtError;
+ class Parent extends React.Component {
+ render() {
+ return ;
+ }
+ componentDidMount() {
+ errors.push('parent sad');
+ throw new Error('parent sad');
+ }
+ }
+ class Child extends React.Component {
+ render() {
+ return ;
+ }
+ componentDidMount() {
+ errors.push('child sad');
+ throw new Error('child sad');
+ }
+ }
+
+ const container = document.createElement('div');
+ try {
+ // Here, we test the behavior where there is no error boundary and we
+ // delegate to the host root.
+ ReactDOM.render(, container);
+ } catch (e) {
+ if (e.message !== 'parent sad' && e.message !== 'child sad') {
+ throw e;
+ }
+ caughtError = e;
+ }
+
+ expect(errors).toEqual(['child sad', 'parent sad']);
+ // Error should be the first thrown
+ expect(caughtError.message).toBe('child sad');
+ });
+
+ it('propagates uncaught error inside unbatched initial mount', () => {
+ function Foo() {
+ throw new Error('foo error');
+ }
+ const container = document.createElement('div');
+ expect(() => {
+ ReactDOM.unstable_batchedUpdates(() => {
+ ReactDOM.render(, container);
+ });
+ }).toThrow('foo error');
+ });
+
+ it('handles errors that occur in before-mutation commit hook', () => {
+ const errors = [];
+ let caughtError;
+ class Parent extends React.Component {
+ getSnapshotBeforeUpdate() {
+ errors.push('parent sad');
+ throw new Error('parent sad');
+ }
+ componentDidUpdate() {}
+ render() {
+ return ;
+ }
+ }
+ class Child extends React.Component {
+ getSnapshotBeforeUpdate() {
+ errors.push('child sad');
+ throw new Error('child sad');
+ }
+ componentDidUpdate() {}
+ render() {
+ return ;
+ }
+ }
+
+ const container = document.createElement('div');
+ ReactDOM.render(, container);
+ try {
+ ReactDOM.render(, container);
+ } catch (e) {
+ if (e.message !== 'parent sad' && e.message !== 'child sad') {
+ throw e;
+ }
+ caughtError = e;
+ }
+
+ expect(errors).toEqual(['child sad', 'parent sad']);
+ // Error should be the first thrown
+ expect(caughtError.message).toBe('child sad');
+ });
+});