diff --git a/packages/react-art/src/ReactARTHostConfig.js b/packages/react-art/src/ReactARTHostConfig.js
index b1f80ccc2be85..4ec8e3a4e6f2e 100644
--- a/packages/react-art/src/ReactARTHostConfig.js
+++ b/packages/react-art/src/ReactARTHostConfig.js
@@ -452,6 +452,6 @@ export function detachDeletedInstance(node: Instance): void {
// noop
}
-export function logHydrationError(config, error) {
+export function logRecoverableError(config, error) {
// noop
}
diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
index bcf0aaa461628..c2b474c1b1a40 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
@@ -1898,7 +1898,7 @@ describe('ReactDOMFizzServer', () => {
// falls back to client rendering.
isClient = true;
ReactDOM.hydrateRoot(container, , {
- onHydrationError(error) {
+ onRecoverableError(error) {
Scheduler.unstable_yieldValue(error.message);
},
});
@@ -1982,7 +1982,7 @@ describe('ReactDOMFizzServer', () => {
// Hydrate the tree. Child will throw during render.
isClient = true;
ReactDOM.hydrateRoot(container, , {
- onHydrationError(error) {
+ onRecoverableError(error) {
// TODO: We logged a hydration error, but the same error ends up
// being thrown during the fallback to client rendering, too. Maybe
// we should only log if the client render succeeds.
@@ -2063,7 +2063,7 @@ describe('ReactDOMFizzServer', () => {
// falls back to client rendering.
isClient = true;
ReactDOM.hydrateRoot(container, , {
- onHydrationError(error) {
+ onRecoverableError(error) {
Scheduler.unstable_yieldValue(error.message);
},
});
@@ -2100,4 +2100,64 @@ describe('ReactDOMFizzServer', () => {
expect(span3Ref.current).toBe(span3);
},
);
+
+ it('logs regular (non-hydration) errors when the UI recovers', async () => {
+ let shouldThrow = true;
+
+ function A() {
+ if (shouldThrow) {
+ Scheduler.unstable_yieldValue('Oops!');
+ throw new Error('Oops!');
+ }
+ Scheduler.unstable_yieldValue('A');
+ return 'A';
+ }
+
+ function B() {
+ Scheduler.unstable_yieldValue('B');
+ return 'B';
+ }
+
+ function App() {
+ return (
+ <>
+
+
+ >
+ );
+ }
+
+ const root = ReactDOM.createRoot(container, {
+ onRecoverableError(error) {
+ Scheduler.unstable_yieldValue(
+ 'Logged a recoverable error: ' + error.message,
+ );
+ },
+ });
+ React.startTransition(() => {
+ root.render();
+ });
+
+ // Partially render A, but yield before the render has finished
+ expect(Scheduler).toFlushAndYieldThrough(['Oops!', 'Oops!']);
+
+ // React will try rendering again synchronously. During the retry, A will
+ // not throw. This simulates a concurrent data race that is fixed by
+ // blocking the main thread.
+ shouldThrow = false;
+ expect(Scheduler).toFlushAndYield([
+ // Finish initial render attempt
+ 'B',
+
+ // Render again, synchronously
+ 'A',
+ 'B',
+
+ // Log the error
+ 'Logged a recoverable error: Oops!',
+ ]);
+
+ // UI looks normal
+ expect(container.textContent).toEqual('AB');
+ });
});
diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
index 1a98b8e8f877e..a5ca716bdb96d 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
@@ -209,7 +209,7 @@ describe('ReactDOMServerPartialHydration', () => {
// hydrating anyway.
suspend = true;
ReactDOM.hydrateRoot(container, , {
- onHydrationError(error) {
+ onRecoverableError(error) {
Scheduler.unstable_yieldValue(error.message);
},
});
@@ -299,7 +299,7 @@ describe('ReactDOMServerPartialHydration', () => {
client = true;
ReactDOM.hydrateRoot(container, , {
- onHydrationError(error) {
+ onRecoverableError(error) {
Scheduler.unstable_yieldValue(error.message);
},
});
@@ -3052,13 +3052,27 @@ describe('ReactDOMServerPartialHydration', () => {
expect(() => {
act(() => {
- ReactDOM.hydrateRoot(container, );
+ ReactDOM.hydrateRoot(container, , {
+ onRecoverableError(error) {
+ Scheduler.unstable_yieldValue(
+ 'Log recoverable error: ' + error.message,
+ );
+ },
+ });
});
}).toErrorDev(
'Warning: An error occurred during hydration. ' +
'The server HTML was replaced with client content in
.',
{withoutStack: true},
);
+ expect(Scheduler).toHaveYielded([
+ 'Log recoverable error: An error occurred during hydration. The server ' +
+ 'HTML was replaced with client content',
+ // TODO: There were multiple mismatches in a single container. Should
+ // we attempt to de-dupe them?
+ 'Log recoverable error: An error occurred during hydration. The server ' +
+ 'HTML was replaced with client content',
+ ]);
// We show fallback state when mismatch happens at root
expect(container.innerHTML).toEqual(
diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js
index cd2fad2fdb873..fdd8b38e312b7 100644
--- a/packages/react-dom/src/client/ReactDOMHostConfig.js
+++ b/packages/react-dom/src/client/ReactDOMHostConfig.js
@@ -379,15 +379,15 @@ export function getCurrentEventPriority(): * {
return getEventPriority(currentEvent.type);
}
-export function logHydrationError(
+export function logRecoverableError(
config: ErrorLoggingConfig,
error: mixed,
): void {
- const onHydrationError = config;
- if (onHydrationError !== null) {
+ const onRecoverableError = config;
+ if (onRecoverableError !== null) {
// Schedule a callback to invoke the user-provided logging function.
scheduleCallback(IdlePriority, () => {
- onHydrationError(error);
+ onRecoverableError(error);
});
} else {
// Default behavior is to rethrow the error in a separate task. This will
@@ -1094,6 +1094,8 @@ export function didNotFindHydratableSuspenseInstance(
export function errorHydratingContainer(parentContainer: Container): void {
if (__DEV__) {
+ // TODO: This gets logged by onRecoverableError, too, so we should be
+ // able to remove it.
console.error(
'An error occurred during hydration. The server HTML was replaced with client content in <%s>.',
parentContainer.nodeName.toLowerCase(),
diff --git a/packages/react-dom/src/client/ReactDOMRoot.js b/packages/react-dom/src/client/ReactDOMRoot.js
index caf1f78c4801c..fe6b6ee31f773 100644
--- a/packages/react-dom/src/client/ReactDOMRoot.js
+++ b/packages/react-dom/src/client/ReactDOMRoot.js
@@ -24,6 +24,7 @@ export type CreateRootOptions = {
unstable_strictMode?: boolean,
unstable_concurrentUpdatesByDefault?: boolean,
identifierPrefix?: string,
+ onRecoverableError?: (error: mixed) => void,
...
};
@@ -36,7 +37,7 @@ export type HydrateRootOptions = {
unstable_strictMode?: boolean,
unstable_concurrentUpdatesByDefault?: boolean,
identifierPrefix?: string,
- onHydrationError?: (error: mixed) => void,
+ onRecoverableError?: (error: mixed) => void,
...
};
@@ -144,6 +145,7 @@ export function createRoot(
let isStrictMode = false;
let concurrentUpdatesByDefaultOverride = false;
let identifierPrefix = '';
+ let onRecoverableError = null;
if (options !== null && options !== undefined) {
if (__DEV__) {
if ((options: any).hydrate) {
@@ -164,6 +166,9 @@ export function createRoot(
if (options.identifierPrefix !== undefined) {
identifierPrefix = options.identifierPrefix;
}
+ if (options.onRecoverableError !== undefined) {
+ onRecoverableError = options.onRecoverableError;
+ }
}
const root = createContainer(
@@ -174,7 +179,7 @@ export function createRoot(
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
- null,
+ onRecoverableError,
);
markContainerAsRoot(root.current, container);
@@ -215,7 +220,7 @@ export function hydrateRoot(
let isStrictMode = false;
let concurrentUpdatesByDefaultOverride = false;
let identifierPrefix = '';
- let onHydrationError = null;
+ let onRecoverableError = null;
if (options !== null && options !== undefined) {
if (options.unstable_strictMode === true) {
isStrictMode = true;
@@ -229,8 +234,8 @@ export function hydrateRoot(
if (options.identifierPrefix !== undefined) {
identifierPrefix = options.identifierPrefix;
}
- if (options.onHydrationError !== undefined) {
- onHydrationError = options.onHydrationError;
+ if (options.onRecoverableError !== undefined) {
+ onRecoverableError = options.onRecoverableError;
}
}
@@ -242,7 +247,7 @@ export function hydrateRoot(
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
- onHydrationError,
+ onRecoverableError,
);
markContainerAsRoot(root.current, container);
// This can't be a comment node since hydration doesn't work on comment nodes anyway.
diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js
index 3d2f890387678..e720c2e12534e 100644
--- a/packages/react-native-renderer/src/ReactFabricHostConfig.js
+++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js
@@ -528,7 +528,7 @@ export function detachDeletedInstance(node: Instance): void {
// noop
}
-export function logHydrationError(
+export function logRecoverableError(
config: ErrorLoggingConfig,
error: mixed,
): void {
diff --git a/packages/react-native-renderer/src/ReactNativeHostConfig.js b/packages/react-native-renderer/src/ReactNativeHostConfig.js
index bc7c859c4c858..27df360718aee 100644
--- a/packages/react-native-renderer/src/ReactNativeHostConfig.js
+++ b/packages/react-native-renderer/src/ReactNativeHostConfig.js
@@ -516,7 +516,7 @@ export function detachDeletedInstance(node: Instance): void {
// noop
}
-export function logHydrationError(
+export function logRecoverableError(
config: ErrorLoggingConfig,
error: mixed,
): void {
diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js
index c93b5eb6e91dd..411757c0436be 100644
--- a/packages/react-noop-renderer/src/createReactNoop.js
+++ b/packages/react-noop-renderer/src/createReactNoop.js
@@ -467,7 +467,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
detachDeletedInstance() {},
- logHydrationError() {
+ logRecoverableError() {
// no-op
},
};
diff --git a/packages/react-reconciler/src/ReactFiberThrow.new.js b/packages/react-reconciler/src/ReactFiberThrow.new.js
index 60903d236e0d5..c2b2a9a2fa5e7 100644
--- a/packages/react-reconciler/src/ReactFiberThrow.new.js
+++ b/packages/react-reconciler/src/ReactFiberThrow.new.js
@@ -37,7 +37,7 @@ import {
import {
supportsPersistence,
getOffscreenContainerProps,
- logHydrationError,
+ logRecoverableError,
} from './ReactFiberHostConfig';
import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent.new';
import {NoMode, ConcurrentMode, DebugTracingMode} from './ReactTypeOfMode';
@@ -515,7 +515,7 @@ function throwException(
// probably want to log any error that is recovered from without
// triggering an error boundary — or maybe even those, too. Need to
// figure out the right API.
- logHydrationError(root.errorLoggingConfig, value);
+ logRecoverableError(root.errorLoggingConfig, value);
return;
}
} else {
@@ -526,7 +526,7 @@ function throwException(
// We didn't find a boundary that could handle this type of exception. Start
// over and traverse parent path again, this time treating the exception
// as an error.
- renderDidError();
+ renderDidError(value);
value = createCapturedValue(value, sourceFiber);
let workInProgress = returnFiber;
diff --git a/packages/react-reconciler/src/ReactFiberThrow.old.js b/packages/react-reconciler/src/ReactFiberThrow.old.js
index 6b7f4bf6055b4..3ae5df1f93414 100644
--- a/packages/react-reconciler/src/ReactFiberThrow.old.js
+++ b/packages/react-reconciler/src/ReactFiberThrow.old.js
@@ -37,7 +37,7 @@ import {
import {
supportsPersistence,
getOffscreenContainerProps,
- logHydrationError,
+ logRecoverableError,
} from './ReactFiberHostConfig';
import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent.old';
import {NoMode, ConcurrentMode, DebugTracingMode} from './ReactTypeOfMode';
@@ -515,7 +515,7 @@ function throwException(
// probably want to log any error that is recovered from without
// triggering an error boundary — or maybe even those, too. Need to
// figure out the right API.
- logHydrationError(root.errorLoggingConfig, value);
+ logRecoverableError(root.errorLoggingConfig, value);
return;
}
} else {
@@ -526,7 +526,7 @@ function throwException(
// We didn't find a boundary that could handle this type of exception. Start
// over and traverse parent path again, this time treating the exception
// as an error.
- renderDidError();
+ renderDidError(value);
value = createCapturedValue(value, sourceFiber);
let workInProgress = returnFiber;
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
index b4b333547c194..f05174cf34f9c 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
@@ -76,6 +76,7 @@ import {
supportsMicrotasks,
errorHydratingContainer,
scheduleMicrotask,
+ logRecoverableError,
} from './ReactFiberHostConfig';
import {
@@ -296,6 +297,7 @@ let workInProgressRootInterleavedUpdatedLanes: Lanes = NoLanes;
let workInProgressRootRenderPhaseUpdatedLanes: Lanes = NoLanes;
// Lanes that were pinged (in an interleaved event) during this render.
let workInProgressRootPingedLanes: Lanes = NoLanes;
+let workInProgressRootConcurrentErrors: Array
| null = null;
// The most recent time we committed a fallback. This lets us ensure a train
// model where we don't commit new loading states in too quick succession.
@@ -896,18 +898,40 @@ function recoverFromConcurrentError(root, errorRetryLanes) {
let exitStatus;
+ let recoverableErrors = workInProgressRootConcurrentErrors;
const MAX_ERROR_RETRY_ATTEMPTS = 50;
for (let i = 0; i < MAX_ERROR_RETRY_ATTEMPTS; i++) {
exitStatus = renderRootSync(root, errorRetryLanes);
- if (
- exitStatus === RootErrored &&
- workInProgressRootRenderPhaseUpdatedLanes !== NoLanes
- ) {
- // There was a render phase update during this render. Some internal React
- // implementation details may use this as a trick to schedule another
- // render pass. To protect against an inifinite loop, eventually
- // we'll give up.
- continue;
+ if (exitStatus !== RootErrored) {
+ // Successfully finished rendering
+ if (recoverableErrors !== null) {
+ // Although we recovered the UI without surfacing an error, we should
+ // still log the errors so they can be fixed.
+ for (let j = 0; j < recoverableErrors.length; j++) {
+ const recoverableError = recoverableErrors[j];
+ logRecoverableError(root.errorLoggingConfig, recoverableError);
+ }
+ }
+ } else {
+ // The UI failed to recover.
+ if (workInProgressRootRenderPhaseUpdatedLanes !== NoLanes) {
+ // There was a render phase update during this render. Some internal React
+ // implementation details may use this as a trick to schedule another
+ // render pass. To protect against an inifinite loop, eventually
+ // we'll give up.
+ //
+ // Add the newly thrown errors to the list of recoverable errors. If we
+ // eventually recover, we'll log them. Otherwise, we'll surface the
+ // error to the UI.
+ if (workInProgressRootConcurrentErrors !== null) {
+ if (recoverableErrors === null) {
+ recoverableErrors = workInProgressRootConcurrentErrors;
+ } else {
+ recoverableErrors.concat(workInProgressRootConcurrentErrors);
+ }
+ }
+ continue;
+ }
}
break;
}
@@ -1336,6 +1360,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
workInProgressRootInterleavedUpdatedLanes = NoLanes;
workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
workInProgressRootPingedLanes = NoLanes;
+ workInProgressRootConcurrentErrors = null;
enqueueInterleavedUpdates();
@@ -1490,10 +1515,15 @@ export function renderDidSuspendDelayIfPossible(): void {
}
}
-export function renderDidError() {
+export function renderDidError(error: mixed) {
if (workInProgressRootExitStatus !== RootSuspendedWithDelay) {
workInProgressRootExitStatus = RootErrored;
}
+ if (workInProgressRootConcurrentErrors === null) {
+ workInProgressRootConcurrentErrors = [error];
+ } else {
+ workInProgressRootConcurrentErrors.push(error);
+ }
}
// Called during render to determine if anything has suspended.
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
index d8bb61af50c84..7bc8edc872f96 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
@@ -76,6 +76,7 @@ import {
supportsMicrotasks,
errorHydratingContainer,
scheduleMicrotask,
+ logRecoverableError,
} from './ReactFiberHostConfig';
import {
@@ -296,6 +297,7 @@ let workInProgressRootInterleavedUpdatedLanes: Lanes = NoLanes;
let workInProgressRootRenderPhaseUpdatedLanes: Lanes = NoLanes;
// Lanes that were pinged (in an interleaved event) during this render.
let workInProgressRootPingedLanes: Lanes = NoLanes;
+let workInProgressRootConcurrentErrors: Array | null = null;
// The most recent time we committed a fallback. This lets us ensure a train
// model where we don't commit new loading states in too quick succession.
@@ -896,18 +898,40 @@ function recoverFromConcurrentError(root, errorRetryLanes) {
let exitStatus;
+ let recoverableErrors = workInProgressRootConcurrentErrors;
const MAX_ERROR_RETRY_ATTEMPTS = 50;
for (let i = 0; i < MAX_ERROR_RETRY_ATTEMPTS; i++) {
exitStatus = renderRootSync(root, errorRetryLanes);
- if (
- exitStatus === RootErrored &&
- workInProgressRootRenderPhaseUpdatedLanes !== NoLanes
- ) {
- // There was a render phase update during this render. Some internal React
- // implementation details may use this as a trick to schedule another
- // render pass. To protect against an inifinite loop, eventually
- // we'll give up.
- continue;
+ if (exitStatus !== RootErrored) {
+ // Successfully finished rendering
+ if (recoverableErrors !== null) {
+ // Although we recovered the UI without surfacing an error, we should
+ // still log the errors so they can be fixed.
+ for (let j = 0; j < recoverableErrors.length; j++) {
+ const recoverableError = recoverableErrors[j];
+ logRecoverableError(root.errorLoggingConfig, recoverableError);
+ }
+ }
+ } else {
+ // The UI failed to recover.
+ if (workInProgressRootRenderPhaseUpdatedLanes !== NoLanes) {
+ // There was a render phase update during this render. Some internal React
+ // implementation details may use this as a trick to schedule another
+ // render pass. To protect against an inifinite loop, eventually
+ // we'll give up.
+ //
+ // Add the newly thrown errors to the list of recoverable errors. If we
+ // eventually recover, we'll log them. Otherwise, we'll surface the
+ // error to the UI.
+ if (workInProgressRootConcurrentErrors !== null) {
+ if (recoverableErrors === null) {
+ recoverableErrors = workInProgressRootConcurrentErrors;
+ } else {
+ recoverableErrors.concat(workInProgressRootConcurrentErrors);
+ }
+ }
+ continue;
+ }
}
break;
}
@@ -1336,6 +1360,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
workInProgressRootInterleavedUpdatedLanes = NoLanes;
workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
workInProgressRootPingedLanes = NoLanes;
+ workInProgressRootConcurrentErrors = null;
enqueueInterleavedUpdates();
@@ -1490,10 +1515,15 @@ export function renderDidSuspendDelayIfPossible(): void {
}
}
-export function renderDidError() {
+export function renderDidError(error: mixed) {
if (workInProgressRootExitStatus !== RootSuspendedWithDelay) {
workInProgressRootExitStatus = RootErrored;
}
+ if (workInProgressRootConcurrentErrors === null) {
+ workInProgressRootConcurrentErrors = [error];
+ } else {
+ workInProgressRootConcurrentErrors.push(error);
+ }
}
// Called during render to determine if anything has suspended.
diff --git a/packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js b/packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js
index 4c13ef7c35dfa..16ebb4657d28d 100644
--- a/packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js
+++ b/packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js
@@ -205,6 +205,9 @@ describe('useMutableSourceHydration', () => {
act(() => {
ReactDOM.hydrateRoot(container, , {
mutableSources: [mutableSource],
+ onRecoverableError(error) {
+ Scheduler.unstable_yieldValue('Log error: ' + error.message);
+ },
});
source.value = 'two';
@@ -254,11 +257,17 @@ describe('useMutableSourceHydration', () => {
React.startTransition(() => {
ReactDOM.hydrateRoot(container, , {
mutableSources: [mutableSource],
+ onRecoverableError(error) {
+ Scheduler.unstable_yieldValue('Log error: ' + error.message);
+ },
});
});
} else {
ReactDOM.hydrateRoot(container, , {
mutableSources: [mutableSource],
+ onRecoverableError(error) {
+ Scheduler.unstable_yieldValue('Log error: ' + error.message);
+ },
});
}
expect(Scheduler).toFlushAndYieldThrough(['a:one']);
@@ -269,7 +278,17 @@ describe('useMutableSourceHydration', () => {
'The server HTML was replaced with client content in .',
{withoutStack: true},
);
- expect(Scheduler).toHaveYielded(['a:two', 'b:two']);
+ expect(Scheduler).toHaveYielded([
+ 'a:two',
+ 'b:two',
+ // TODO: Before onRecoverableError, this error was never surfaced to the
+ // user. The request to file an bug report no longer makes sense.
+ // However, the experimental useMutableSource API is slated for
+ // removal, anyway.
+ 'Log error: Cannot read from mutable source during the current ' +
+ 'render without tearing. This may be a bug in React. Please file ' +
+ 'an issue.',
+ ]);
expect(source.listenerCount).toBe(2);
});
@@ -328,11 +347,17 @@ describe('useMutableSourceHydration', () => {
React.startTransition(() => {
ReactDOM.hydrateRoot(container, fragment, {
mutableSources: [mutableSource],
+ onRecoverableError(error) {
+ Scheduler.unstable_yieldValue('Log error: ' + error.message);
+ },
});
});
} else {
ReactDOM.hydrateRoot(container, fragment, {
mutableSources: [mutableSource],
+ onRecoverableError(error) {
+ Scheduler.unstable_yieldValue('Log error: ' + error.message);
+ },
});
}
expect(Scheduler).toFlushAndYieldThrough(['0:a:one']);
@@ -343,7 +368,17 @@ describe('useMutableSourceHydration', () => {
'The server HTML was replaced with client content in
.',
{withoutStack: true},
);
- expect(Scheduler).toHaveYielded(['0:a:one', '1:b:two']);
+ expect(Scheduler).toHaveYielded([
+ '0:a:one',
+ '1:b:two',
+ // TODO: Before onRecoverableError, this error was never surfaced to the
+ // user. The request to file an bug report no longer makes sense.
+ // However, the experimental useMutableSource API is slated for
+ // removal, anyway.
+ 'Log error: Cannot read from mutable source during the current ' +
+ 'render without tearing. This may be a bug in React. Please file ' +
+ 'an issue.',
+ ]);
});
// @gate !enableSyncDefaultUpdates
diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
index 38cee5f94e11c..8e67bf5517e45 100644
--- a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
@@ -69,7 +69,7 @@ export const prepareScopeUpdate = $$$hostConfig.preparePortalMount;
export const getInstanceFromScope = $$$hostConfig.getInstanceFromScope;
export const getCurrentEventPriority = $$$hostConfig.getCurrentEventPriority;
export const detachDeletedInstance = $$$hostConfig.detachDeletedInstance;
-export const logHydrationError = $$$hostConfig.logHydrationError;
+export const logRecoverableError = $$$hostConfig.logRecoverableError;
// -------------------
// Microtasks
diff --git a/packages/react-test-renderer/src/ReactTestHostConfig.js b/packages/react-test-renderer/src/ReactTestHostConfig.js
index 5279fda0b43f6..266b2c06e58a1 100644
--- a/packages/react-test-renderer/src/ReactTestHostConfig.js
+++ b/packages/react-test-renderer/src/ReactTestHostConfig.js
@@ -317,7 +317,7 @@ export function detachDeletedInstance(node: Instance): void {
// noop
}
-export function logHydrationError(
+export function logRecoverableError(
config: ErrorLoggingConfig,
error: mixed,
): void {