diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js
index 5ddaf5cded404..877832d81627d 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.js
@@ -423,9 +423,15 @@ function finishClassComponent(
// If we're recovering from an error, reconcile twice: first to delete
// all the existing children.
reconcileChildren(current, workInProgress, null, renderExpirationTime);
- // Reset current child so that subsequent reconciliation will always re-add.
+ // Forcefully reset children so that a subsequent reconciliation will always re-add.
// This is important if e.g. an error boundary renders an element of the same type.
- current.child = null;
+ // Concurrent renderer mode will always retry an extra time on failure,
+ // so the fiber we need to reset varies.
+ if (workInProgress.mode & ConcurrentMode) {
+ workInProgress.child = null;
+ } else {
+ current.child = null;
+ }
// Now we can continue reconciling like normal. This has the effect of
// remounting all children regardless of whether their their
// identity matches.
diff --git a/packages/react-reconciler/src/__tests__/ErrorBoundaryReconciliation-test.internal.js b/packages/react-reconciler/src/__tests__/ErrorBoundaryReconciliation-test.internal.js
new file mode 100644
index 0000000000000..5c1e800734e8a
--- /dev/null
+++ b/packages/react-reconciler/src/__tests__/ErrorBoundaryReconciliation-test.internal.js
@@ -0,0 +1,111 @@
+const jestDiff = require('jest-diff');
+
+describe('ErrorBoundaryReconciliation', () => {
+ let BrokenRender;
+ let DidCatchErrorBoundary;
+ let GetDerivedErrorBoundary;
+ let React;
+ let ReactFeatureFlags;
+ let ReactTestRenderer;
+ let span;
+
+ beforeEach(() => {
+ jest.resetModules();
+
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
+ ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
+ ReactTestRenderer = require('react-test-renderer');
+ React = require('react');
+
+ DidCatchErrorBoundary = class extends React.Component {
+ state = {error: null};
+ componentDidCatch(error) {
+ this.setState({error});
+ }
+ render() {
+ return this.state.error
+ ? React.createElement(this.props.fallbackTagName, {
+ prop: 'ErrorBoundary',
+ })
+ : this.props.children;
+ }
+ };
+
+ GetDerivedErrorBoundary = class extends React.Component {
+ state = {error: null};
+ static getDerivedStateFromCatch(error) {
+ return {error};
+ }
+ render() {
+ return this.state.error
+ ? React.createElement(this.props.fallbackTagName, {
+ prop: 'ErrorBoundary',
+ })
+ : this.props.children;
+ }
+ };
+
+ const InvalidType = undefined;
+ BrokenRender = ({fail}) =>
+ fail ? : ;
+
+ function toHaveRenderedChildren(renderer, children) {
+ let actual, expected;
+ try {
+ actual = renderer.toJSON();
+ expected = ReactTestRenderer.create(children).toJSON();
+ expect(actual).toEqual(expected);
+ } catch (error) {
+ return {
+ message: () => jestDiff(expected, actual),
+ pass: false,
+ };
+ }
+ return {pass: true};
+ }
+ expect.extend({toHaveRenderedChildren});
+ });
+
+ [true, false].forEach(isConcurrent => {
+ function sharedTest(ErrorBoundary, fallbackTagName) {
+ const renderer = ReactTestRenderer.create(
+
+
+ ,
+ {unstable_isConcurrent: isConcurrent},
+ );
+ if (isConcurrent) {
+ renderer.unstable_flushAll();
+ }
+ expect(renderer).toHaveRenderedChildren();
+
+ expect(() => {
+ renderer.update(
+
+
+ ,
+ );
+ if (isConcurrent) {
+ renderer.unstable_flushAll();
+ }
+ }).toWarnDev(isConcurrent ? ['invalid', 'invalid'] : ['invalid']);
+ expect(renderer).toHaveRenderedChildren(
+ React.createElement(fallbackTagName, {prop: 'ErrorBoundary'}),
+ );
+ }
+
+ describe(isConcurrent ? 'concurrent' : 'sync', () => {
+ it('componentDidCatch can recover by rendering an element of the same type', () =>
+ sharedTest(DidCatchErrorBoundary, 'span'));
+
+ it('componentDidCatch can recover by rendering an element of a different type', () =>
+ sharedTest(DidCatchErrorBoundary, 'div'));
+
+ it('getDerivedStateFromCatch can recover by rendering an element of the same type', () =>
+ sharedTest(GetDerivedErrorBoundary, 'span'));
+
+ it('getDerivedStateFromCatch can recover by rendering an element of a different type', () =>
+ sharedTest(GetDerivedErrorBoundary, 'div'));
+ });
+ });
+});