Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add onRecoverableError option to hydrateRoot, createRoot #23207

Merged
merged 8 commits into from
Feb 4, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/react-art/src/ReactARTHostConfig.js
Original file line number Diff line number Diff line change
@@ -451,3 +451,7 @@ export function preparePortalMount(portalInstance: any): void {
export function detachDeletedInstance(node: Instance): void {
// noop
}

export function logRecoverableError(error) {
// noop
}
118 changes: 109 additions & 9 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
@@ -1723,12 +1723,22 @@ describe('ReactDOMFizzServer', () => {
});
expect(Scheduler).toHaveYielded(['server']);

ReactDOM.hydrateRoot(container, <App />);
ReactDOM.hydrateRoot(container, <App />, {
onRecoverableError(error) {
Scheduler.unstable_yieldValue(
'Log recoverable error: ' + error.message,
);
},
});

if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
expect(() => {
// The first paint switches to client rendering due to mismatch
expect(Scheduler).toFlushUntilNextPaint(['client']);
expect(Scheduler).toFlushUntilNextPaint([
'client',
'Log recoverable error: An error occurred during hydration. ' +
'The server HTML was replaced with client content',
]);
}).toErrorDev(
'Warning: An error occurred during hydration. The server HTML was replaced with client content',
{withoutStack: true},
@@ -1805,13 +1815,23 @@ describe('ReactDOMFizzServer', () => {
});
expect(Scheduler).toHaveYielded(['server']);

ReactDOM.hydrateRoot(container, <App />);
ReactDOM.hydrateRoot(container, <App />, {
onRecoverableError(error) {
Scheduler.unstable_yieldValue(
'Log recoverable error: ' + error.message,
);
},
});

if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
// The first paint uses the client due to mismatch forcing client render
expect(() => {
// The first paint switches to client rendering due to mismatch
expect(Scheduler).toFlushUntilNextPaint(['client']);
expect(Scheduler).toFlushUntilNextPaint([
'client',
'Log recoverable error: An error occurred during hydration. ' +
'The server HTML was replaced with client content',
]);
}).toErrorDev(
'Warning: An error occurred during hydration. The server HTML was replaced with client content',
{withoutStack: true},
@@ -1897,9 +1917,15 @@ describe('ReactDOMFizzServer', () => {
// Hydrate the tree. Child will throw during hydration, but not when it
// falls back to client rendering.
isClient = true;
ReactDOM.hydrateRoot(container, <App />);
ReactDOM.hydrateRoot(container, <App />, {
onRecoverableError(error) {
Scheduler.unstable_yieldValue(error.message);
},
});

expect(Scheduler).toFlushAndYield(['Yay!']);
// An error logged but instead of surfacing it to the UI, we switched
// to client rendering.
expect(Scheduler).toFlushAndYield(['Yay!', 'Hydration error']);
expect(getVisibleChildren(container)).toEqual(
<div>
<span />
@@ -1975,8 +2001,16 @@ describe('ReactDOMFizzServer', () => {

// Hydrate the tree. Child will throw during render.
isClient = true;
ReactDOM.hydrateRoot(container, <App />);
ReactDOM.hydrateRoot(container, <App />, {
onRecoverableError(error) {
Scheduler.unstable_yieldValue(
'Log recoverable error: ' + error.message,
);
},
});

// Because we failed to recover from the error, onRecoverableError
// shouldn't be called.
expect(Scheduler).toFlushAndYield([]);
expect(getVisibleChildren(container)).toEqual('Oops!');
},
@@ -2049,9 +2083,15 @@ describe('ReactDOMFizzServer', () => {
// Hydrate the tree. Child will throw during hydration, but not when it
// falls back to client rendering.
isClient = true;
ReactDOM.hydrateRoot(container, <App />);
ReactDOM.hydrateRoot(container, <App />, {
onRecoverableError(error) {
Scheduler.unstable_yieldValue(error.message);
},
});

expect(Scheduler).toFlushAndYield([]);
// An error logged but instead of surfacing it to the UI, we switched
// to client rendering.
expect(Scheduler).toFlushAndYield(['Hydration error']);
expect(getVisibleChildren(container)).toEqual(
<div>
<span />
@@ -2081,4 +2121,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 (
<>
<A />
<B />
</>
);
}

const root = ReactDOM.createRoot(container, {
onRecoverableError(error) {
Scheduler.unstable_yieldValue(
'Logged a recoverable error: ' + error.message,
);
},
});
React.startTransition(() => {
root.render(<App />);
});

// 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');
});
});
Original file line number Diff line number Diff line change
@@ -208,7 +208,11 @@ describe('ReactDOMServerPartialHydration', () => {
// On the client we don't have all data yet but we want to start
// hydrating anyway.
suspend = true;
ReactDOM.hydrateRoot(container, <App />);
ReactDOM.hydrateRoot(container, <App />, {
onRecoverableError(error) {
Scheduler.unstable_yieldValue(error.message);
},
});
if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
Scheduler.unstable_flushAll();
} else {
@@ -290,7 +294,11 @@ describe('ReactDOMServerPartialHydration', () => {
suspend = true;
client = true;

ReactDOM.hydrateRoot(container, <App />);
ReactDOM.hydrateRoot(container, <App />, {
onRecoverableError(error) {
Scheduler.unstable_yieldValue(error.message);
},
});
expect(Scheduler).toFlushAndYield([
'Suspend',
'Component',
@@ -316,12 +324,16 @@ describe('ReactDOMServerPartialHydration', () => {
'Component',
'Component',
'Component',

// second pass as client render
'Hello',
'Component',
'Component',
'Component',
'Component',

// Hydration mismatch is logged
'An error occurred during hydration. The server HTML was replaced with client content',
]);

// Client rendered - suspense comment nodes removed
@@ -573,9 +585,19 @@ describe('ReactDOMServerPartialHydration', () => {

expect(() => {
act(() => {
ReactDOM.hydrateRoot(container, <App hasB={false} />);
ReactDOM.hydrateRoot(container, <App hasB={false} />, {
onRecoverableError(error) {
Scheduler.unstable_yieldValue(error.message);
},
});
});
}).toErrorDev('Did not expect server HTML to contain a <span> in <div>');
if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
expect(Scheduler).toHaveYielded([
'An error occurred during hydration. The server HTML was replaced ' +
'with client content',
]);
}

expect(container.innerHTML).toContain('<span>A</span>');
expect(container.innerHTML).not.toContain('<span>B</span>');
@@ -2997,7 +3019,13 @@ describe('ReactDOMServerPartialHydration', () => {
const span = container.getElementsByTagName('span')[0];
expect(span.innerHTML).toBe('Hidden child');

ReactDOM.hydrateRoot(container, <App />);
ReactDOM.hydrateRoot(container, <App />, {
onRecoverableError(error) {
Scheduler.unstable_yieldValue(
'Log recoverable error: ' + error.message,
);
},
});

Scheduler.unstable_flushAll();
expect(ref.current).toBe(span);
@@ -3142,13 +3170,27 @@ describe('ReactDOMServerPartialHydration', () => {

expect(() => {
act(() => {
ReactDOM.hydrateRoot(container, <App />);
ReactDOM.hydrateRoot(container, <App />, {
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 <div>.',
{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(
14 changes: 14 additions & 0 deletions packages/react-dom/src/client/ReactDOMHostConfig.js
Original file line number Diff line number Diff line change
@@ -374,6 +374,18 @@ export function getCurrentEventPriority(): * {
return getEventPriority(currentEvent.type);
}

/* global reportError */
export const logRecoverableError =
typeof reportError === 'function'
? // In modern browsers, reportError will dispatch an error event,
// emulating an uncaught JavaScript error.
reportError
: (error: mixed) => {
// In older browsers and test environments, fallback to console.error.
// eslint-disable-next-line react-internal/no-production-logging, react-internal/warning-args
console.error(error);
};

export const isPrimaryRenderer = true;
export const warnsIfNotActing = true;
// This initialization code may run even on server environments
@@ -1070,6 +1082,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(),
1 change: 1 addition & 0 deletions packages/react-dom/src/client/ReactDOMLegacy.js
Original file line number Diff line number Diff line change
@@ -122,6 +122,7 @@ function legacyCreateRootFromDOMContainer(
false, // isStrictMode
false, // concurrentUpdatesByDefaultOverride,
'', // identifierPrefix
null,
);
markContainerAsRoot(root.current, container);

12 changes: 12 additions & 0 deletions packages/react-dom/src/client/ReactDOMRoot.js
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@ export type CreateRootOptions = {
unstable_strictMode?: boolean,
unstable_concurrentUpdatesByDefault?: boolean,
identifierPrefix?: string,
onRecoverableError?: (error: mixed) => void,
...
};

@@ -36,6 +37,7 @@ export type HydrateRootOptions = {
unstable_strictMode?: boolean,
unstable_concurrentUpdatesByDefault?: boolean,
identifierPrefix?: string,
onRecoverableError?: (error: mixed) => void,
...
};

@@ -143,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) {
@@ -163,6 +166,9 @@ export function createRoot(
if (options.identifierPrefix !== undefined) {
identifierPrefix = options.identifierPrefix;
}
if (options.onRecoverableError !== undefined) {
onRecoverableError = options.onRecoverableError;
}
}

const root = createContainer(
@@ -173,6 +179,7 @@ export function createRoot(
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onRecoverableError,
);
markContainerAsRoot(root.current, container);

@@ -213,6 +220,7 @@ export function hydrateRoot(
let isStrictMode = false;
let concurrentUpdatesByDefaultOverride = false;
let identifierPrefix = '';
let onRecoverableError = null;
if (options !== null && options !== undefined) {
if (options.unstable_strictMode === true) {
isStrictMode = true;
@@ -226,6 +234,9 @@ export function hydrateRoot(
if (options.identifierPrefix !== undefined) {
identifierPrefix = options.identifierPrefix;
}
if (options.onRecoverableError !== undefined) {
onRecoverableError = options.onRecoverableError;
}
}

const root = createContainer(
@@ -236,6 +247,7 @@ export function hydrateRoot(
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onRecoverableError,
);
markContainerAsRoot(root.current, container);
// This can't be a comment node since hydration doesn't work on comment nodes anyway.
1 change: 1 addition & 0 deletions packages/react-native-renderer/src/ReactFabric.js
Original file line number Diff line number Diff line change
@@ -214,6 +214,7 @@ function render(
false,
null,
'',
null,
);
roots.set(containerTag, root);
}
4 changes: 4 additions & 0 deletions packages/react-native-renderer/src/ReactFabricHostConfig.js
Original file line number Diff line number Diff line change
@@ -525,3 +525,7 @@ export function preparePortalMount(portalInstance: Instance): void {
export function detachDeletedInstance(node: Instance): void {
// noop
}

export function logRecoverableError(error: mixed): void {
// noop
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

console.error or something maybe to start with?

}
4 changes: 4 additions & 0 deletions packages/react-native-renderer/src/ReactNativeHostConfig.js
Original file line number Diff line number Diff line change
@@ -513,3 +513,7 @@ export function preparePortalMount(portalInstance: Instance): void {
export function detachDeletedInstance(node: Instance): void {
// noop
}

export function logRecoverableError(error: mixed): void {
// noop
}
1 change: 1 addition & 0 deletions packages/react-native-renderer/src/ReactNativeRenderer.js
Original file line number Diff line number Diff line change
@@ -210,6 +210,7 @@ function render(
false,
null,
'',
null,
);
roots.set(containerTag, root);
}
17 changes: 16 additions & 1 deletion packages/react-noop-renderer/src/createReactNoop.js
Original file line number Diff line number Diff line change
@@ -466,6 +466,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
},

detachDeletedInstance() {},

logRecoverableError() {
// no-op
},
};

const hostConfig = useMutation
@@ -954,7 +958,16 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
if (!root) {
const container = {rootID: rootID, pendingChildren: [], children: []};
rootContainers.set(rootID, container);
root = NoopRenderer.createContainer(container, tag, false, null, null);
root = NoopRenderer.createContainer(
container,
tag,
false,
null,
null,
false,
'',
null,
);
roots.set(rootID, root);
}
return root.current.stateNode.containerInfo;
@@ -975,6 +988,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
null,
false,
'',
null,
);
return {
_Scheduler: Scheduler,
@@ -1004,6 +1018,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
null,
false,
'',
null,
);
return {
_Scheduler: Scheduler,
7 changes: 7 additions & 0 deletions packages/react-reconciler/src/ReactFiberCompleteWork.new.js
Original file line number Diff line number Diff line change
@@ -131,6 +131,7 @@ import {
resetHydrationState,
getIsHydrating,
hasUnhydratedTailNodes,
upgradeHydrationErrorsToRecoverable,
} from './ReactFiberHydrationContext.new';
import {
enableSuspenseCallback,
@@ -1099,6 +1100,12 @@ function completeWork(
return null;
}
}

// Successfully completed this tree. If this was a forced client render,
// there may have been recoverable errors during first hydration
// attempt. If so, add them to a queue so we can log them in the
// commit phase.
upgradeHydrationErrorsToRecoverable();
}

if ((workInProgress.flags & DidCapture) !== NoFlags) {
7 changes: 7 additions & 0 deletions packages/react-reconciler/src/ReactFiberCompleteWork.old.js
Original file line number Diff line number Diff line change
@@ -131,6 +131,7 @@ import {
resetHydrationState,
getIsHydrating,
hasUnhydratedTailNodes,
upgradeHydrationErrorsToRecoverable,
} from './ReactFiberHydrationContext.old';
import {
enableSuspenseCallback,
@@ -1099,6 +1100,12 @@ function completeWork(
return null;
}
}

// Successfully completed this tree. If this was a forced client render,
// there may have been recoverable errors during first hydration
// attempt. If so, add them to a queue so we can log them in the
// commit phase.
upgradeHydrationErrorsToRecoverable();
}

if ((workInProgress.flags & DidCapture) !== NoFlags) {
24 changes: 24 additions & 0 deletions packages/react-reconciler/src/ReactFiberHydrationContext.new.js
Original file line number Diff line number Diff line change
@@ -77,13 +77,17 @@ import {
getSuspendedTreeContext,
restoreSuspendedTreeContext,
} from './ReactFiberTreeContext.new';
import {queueRecoverableErrors} from './ReactFiberWorkLoop.new';

// The deepest Fiber on the stack involved in a hydration context.
// This may have been an insertion or a hydration.
let hydrationParentFiber: null | Fiber = null;
let nextHydratableInstance: null | HydratableInstance = null;
let isHydrating: boolean = false;

// Hydration errors that were thrown inside this boundary
let hydrationErrors: Array<mixed> | null = null;

function warnIfHydrating() {
if (__DEV__) {
if (isHydrating) {
@@ -105,6 +109,7 @@ function enterHydrationState(fiber: Fiber): boolean {
);
hydrationParentFiber = fiber;
isHydrating = true;
hydrationErrors = null;
return true;
}

@@ -121,6 +126,7 @@ function reenterHydrationStateFromDehydratedSuspenseInstance(
);
hydrationParentFiber = fiber;
isHydrating = true;
hydrationErrors = null;
if (treeContext !== null) {
restoreSuspendedTreeContext(fiber, treeContext);
}
@@ -601,10 +607,28 @@ function resetHydrationState(): void {
isHydrating = false;
}

export function upgradeHydrationErrorsToRecoverable(): void {
if (hydrationErrors !== null) {
// Successfully completed a forced client render. The errors that occurred
// during the hydration attempt are now recovered. We will log them in
// commit phase, once the entire tree has finished.
queueRecoverableErrors(hydrationErrors);
hydrationErrors = null;
}
}

function getIsHydrating(): boolean {
return isHydrating;
}

export function queueHydrationError(error: mixed): void {
if (hydrationErrors === null) {
hydrationErrors = [error];
} else {
hydrationErrors.push(error);
}
}

export {
warnIfHydrating,
enterHydrationState,
24 changes: 24 additions & 0 deletions packages/react-reconciler/src/ReactFiberHydrationContext.old.js
Original file line number Diff line number Diff line change
@@ -77,13 +77,17 @@ import {
getSuspendedTreeContext,
restoreSuspendedTreeContext,
} from './ReactFiberTreeContext.old';
import {queueRecoverableErrors} from './ReactFiberWorkLoop.old';

// The deepest Fiber on the stack involved in a hydration context.
// This may have been an insertion or a hydration.
let hydrationParentFiber: null | Fiber = null;
let nextHydratableInstance: null | HydratableInstance = null;
let isHydrating: boolean = false;

// Hydration errors that were thrown inside this boundary
let hydrationErrors: Array<mixed> | null = null;

function warnIfHydrating() {
if (__DEV__) {
if (isHydrating) {
@@ -105,6 +109,7 @@ function enterHydrationState(fiber: Fiber): boolean {
);
hydrationParentFiber = fiber;
isHydrating = true;
hydrationErrors = null;
return true;
}

@@ -121,6 +126,7 @@ function reenterHydrationStateFromDehydratedSuspenseInstance(
);
hydrationParentFiber = fiber;
isHydrating = true;
hydrationErrors = null;
if (treeContext !== null) {
restoreSuspendedTreeContext(fiber, treeContext);
}
@@ -601,10 +607,28 @@ function resetHydrationState(): void {
isHydrating = false;
}

export function upgradeHydrationErrorsToRecoverable(): void {
if (hydrationErrors !== null) {
// Successfully completed a forced client render. The errors that occurred
// during the hydration attempt are now recovered. We will log them in
// commit phase, once the entire tree has finished.
queueRecoverableErrors(hydrationErrors);
hydrationErrors = null;
}
}

function getIsHydrating(): boolean {
return isHydrating;
}

export function queueHydrationError(error: mixed): void {
if (hydrationErrors === null) {
hydrationErrors = [error];
} else {
hydrationErrors.push(error);
}
}

export {
warnIfHydrating,
enterHydrationState,
2 changes: 2 additions & 0 deletions packages/react-reconciler/src/ReactFiberReconciler.new.js
Original file line number Diff line number Diff line change
@@ -245,6 +245,7 @@ export function createContainer(
isStrictMode: boolean,
concurrentUpdatesByDefaultOverride: null | boolean,
identifierPrefix: string,
onRecoverableError: null | ((error: mixed) => void),
): OpaqueRoot {
return createFiberRoot(
containerInfo,
@@ -254,6 +255,7 @@ export function createContainer(
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onRecoverableError,
);
}

2 changes: 2 additions & 0 deletions packages/react-reconciler/src/ReactFiberReconciler.old.js
Original file line number Diff line number Diff line change
@@ -245,6 +245,7 @@ export function createContainer(
isStrictMode: boolean,
concurrentUpdatesByDefaultOverride: null | boolean,
identifierPrefix: string,
onRecoverableError: null | ((error: mixed) => void),
): OpaqueRoot {
return createFiberRoot(
containerInfo,
@@ -254,6 +255,7 @@ export function createContainer(
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onRecoverableError,
);
}

15 changes: 14 additions & 1 deletion packages/react-reconciler/src/ReactFiberRoot.new.js
Original file line number Diff line number Diff line change
@@ -30,7 +30,13 @@ import {initializeUpdateQueue} from './ReactUpdateQueue.new';
import {LegacyRoot, ConcurrentRoot} from './ReactRootTags';
import {createCache, retainCache} from './ReactFiberCacheComponent.new';

function FiberRootNode(containerInfo, tag, hydrate, identifierPrefix) {
function FiberRootNode(
containerInfo,
tag,
hydrate,
identifierPrefix,
onRecoverableError,
) {
this.tag = tag;
this.containerInfo = containerInfo;
this.pendingChildren = null;
@@ -57,6 +63,7 @@ function FiberRootNode(containerInfo, tag, hydrate, identifierPrefix) {
this.entanglements = createLaneMap(NoLanes);

this.identifierPrefix = identifierPrefix;
this.onRecoverableError = onRecoverableError;

if (enableCache) {
this.pooledCache = null;
@@ -103,13 +110,19 @@ export function createFiberRoot(
hydrationCallbacks: null | SuspenseHydrationCallbacks,
isStrictMode: boolean,
concurrentUpdatesByDefaultOverride: null | boolean,
// TODO: We have several of these arguments that are conceptually part of the
// host config, but because they are passed in at runtime, we have to thread
// them through the root constructor. Perhaps we should put them all into a
// single type, like a DynamicHostConfig that is defined by the renderer.
identifierPrefix: string,
onRecoverableError: null | ((error: mixed) => void),
): FiberRoot {
const root: FiberRoot = (new FiberRootNode(
containerInfo,
tag,
hydrate,
identifierPrefix,
onRecoverableError,
): any);
if (enableSuspenseCallback) {
root.hydrationCallbacks = hydrationCallbacks;
15 changes: 14 additions & 1 deletion packages/react-reconciler/src/ReactFiberRoot.old.js
Original file line number Diff line number Diff line change
@@ -30,7 +30,13 @@ import {initializeUpdateQueue} from './ReactUpdateQueue.old';
import {LegacyRoot, ConcurrentRoot} from './ReactRootTags';
import {createCache, retainCache} from './ReactFiberCacheComponent.old';

function FiberRootNode(containerInfo, tag, hydrate, identifierPrefix) {
function FiberRootNode(
containerInfo,
tag,
hydrate,
identifierPrefix,
onRecoverableError,
) {
this.tag = tag;
this.containerInfo = containerInfo;
this.pendingChildren = null;
@@ -57,6 +63,7 @@ function FiberRootNode(containerInfo, tag, hydrate, identifierPrefix) {
this.entanglements = createLaneMap(NoLanes);

this.identifierPrefix = identifierPrefix;
this.onRecoverableError = onRecoverableError;

if (enableCache) {
this.pooledCache = null;
@@ -103,13 +110,19 @@ export function createFiberRoot(
hydrationCallbacks: null | SuspenseHydrationCallbacks,
isStrictMode: boolean,
concurrentUpdatesByDefaultOverride: null | boolean,
// TODO: We have several of these arguments that are conceptually part of the
// host config, but because they are passed in at runtime, we have to thread
// them through the root constructor. Perhaps we should put them all into a
// single type, like a DynamicHostConfig that is defined by the renderer.
identifierPrefix: string,
onRecoverableError: null | ((error: mixed) => void),
): FiberRoot {
const root: FiberRoot = (new FiberRootNode(
containerInfo,
tag,
hydrate,
identifierPrefix,
onRecoverableError,
): any);
if (enableSuspenseCallback) {
root.hydrationCallbacks = hydrationCallbacks;
11 changes: 9 additions & 2 deletions packages/react-reconciler/src/ReactFiberThrow.new.js
Original file line number Diff line number Diff line change
@@ -79,7 +79,10 @@ import {
mergeLanes,
pickArbitraryLane,
} from './ReactFiberLane.new';
import {getIsHydrating} from './ReactFiberHydrationContext.new';
import {
getIsHydrating,
queueHydrationError,
} from './ReactFiberHydrationContext.new';

const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;

@@ -507,6 +510,10 @@ function throwException(
root,
rootRenderLanes,
);

// Even though the user may not be affected by this error, we should
// still log it so it can be fixed.
queueHydrationError(value);
return;
}
} else {
@@ -517,7 +524,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;
11 changes: 9 additions & 2 deletions packages/react-reconciler/src/ReactFiberThrow.old.js
Original file line number Diff line number Diff line change
@@ -79,7 +79,10 @@ import {
mergeLanes,
pickArbitraryLane,
} from './ReactFiberLane.old';
import {getIsHydrating} from './ReactFiberHydrationContext.old';
import {
getIsHydrating,
queueHydrationError,
} from './ReactFiberHydrationContext.old';

const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;

@@ -507,6 +510,10 @@ function throwException(
root,
rootRenderLanes,
);

// Even though the user may not be affected by this error, we should
// still log it so it can be fixed.
queueHydrationError(value);
return;
}
} else {
@@ -517,7 +524,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;
79 changes: 68 additions & 11 deletions packages/react-reconciler/src/ReactFiberWorkLoop.new.js
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
import type {StackCursor} from './ReactFiberStack.new';
import type {Flags} from './ReactFiberFlags';
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new';
import type {EventPriority} from './ReactEventPriorities.new';

import {
warnAboutDeprecatedLifecycles,
@@ -76,6 +77,7 @@ import {
supportsMicrotasks,
errorHydratingContainer,
scheduleMicrotask,
logRecoverableError,
} from './ReactFiberHostConfig';

import {
@@ -296,6 +298,11 @@ let workInProgressRootInterleavedUpdatedLanes: Lanes = NoLanes;
let workInProgressRootRenderPhaseUpdatedLanes: Lanes = NoLanes;
// Lanes that were pinged (in an interleaved event) during this render.
let workInProgressRootPingedLanes: Lanes = NoLanes;
// Errors that are thrown during the render phase.
let workInProgressRootConcurrentErrors: Array<mixed> | null = null;
// These are errors that we recovered from without surfacing them to the UI.
// We will log them once the tree commits.
let workInProgressRootRecoverableErrors: Array<mixed> | 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.
@@ -894,13 +901,36 @@ function recoverFromConcurrentError(root, errorRetryLanes) {
}
}

const errorsFromFirstAttempt = workInProgressRootConcurrentErrors;
const exitStatus = renderRootSync(root, errorRetryLanes);
if (exitStatus !== RootErrored) {
// Successfully finished rendering on retry
if (errorsFromFirstAttempt !== null) {
// The errors from the failed first attempt have been recovered. Add
// them to the collection of recoverable errors. We'll log them in the
// commit phase.
queueRecoverableErrors(errorsFromFirstAttempt);
}
} else {
// The UI failed to recover.
}

executionContext = prevExecutionContext;

return exitStatus;
}

export function queueRecoverableErrors(errors: Array<mixed>) {
if (workInProgressRootConcurrentErrors === null) {
workInProgressRootRecoverableErrors = errors;
} else {
workInProgressRootConcurrentErrors = workInProgressRootConcurrentErrors.push.apply(
null,
errors,
);
}
}

function finishConcurrentRender(root, exitStatus, lanes) {
switch (exitStatus) {
case RootIncomplete:
@@ -913,7 +943,7 @@ function finishConcurrentRender(root, exitStatus, lanes) {
case RootErrored: {
// We should have already attempted to retry this tree. If we reached
// this point, it errored again. Commit it.
commitRoot(root);
commitRoot(root, workInProgressRootRecoverableErrors);
break;
}
case RootSuspended: {
@@ -953,14 +983,14 @@ function finishConcurrentRender(root, exitStatus, lanes) {
// lower priority work to do. Instead of committing the fallback
// immediately, wait for more data to arrive.
root.timeoutHandle = scheduleTimeout(
commitRoot.bind(null, root),
commitRoot.bind(null, root, workInProgressRootRecoverableErrors),
msUntilTimeout,
);
break;
}
}
// The work expired. Commit immediately.
commitRoot(root);
commitRoot(root, workInProgressRootRecoverableErrors);
break;
}
case RootSuspendedWithDelay: {
@@ -991,20 +1021,20 @@ function finishConcurrentRender(root, exitStatus, lanes) {
// Instead of committing the fallback immediately, wait for more data
// to arrive.
root.timeoutHandle = scheduleTimeout(
commitRoot.bind(null, root),
commitRoot.bind(null, root, workInProgressRootRecoverableErrors),
msUntilTimeout,
);
break;
}
}

// Commit the placeholder.
commitRoot(root);
commitRoot(root, workInProgressRootRecoverableErrors);
break;
}
case RootCompleted: {
// The work completed. Ready to commit.
commitRoot(root);
commitRoot(root, workInProgressRootRecoverableErrors);
break;
}
default: {
@@ -1124,7 +1154,7 @@ function performSyncWorkOnRoot(root) {
const finishedWork: Fiber = (root.current.alternate: any);
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
commitRoot(root);
commitRoot(root, workInProgressRootRecoverableErrors);

// Before exiting, make sure there's a callback scheduled for the next
// pending level.
@@ -1320,6 +1350,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
workInProgressRootInterleavedUpdatedLanes = NoLanes;
workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
workInProgressRootPingedLanes = NoLanes;
workInProgressRootConcurrentErrors = null;
workInProgressRootRecoverableErrors = null;

enqueueInterleavedUpdates();

@@ -1474,10 +1506,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.
@@ -1781,15 +1818,15 @@ function completeUnitOfWork(unitOfWork: Fiber): void {
}
}

function commitRoot(root) {
function commitRoot(root: FiberRoot, recoverableErrors: null | Array<mixed>) {
// TODO: This no longer makes any sense. We already wrap the mutation and
// layout phases. Should be able to remove.
const previousUpdateLanePriority = getCurrentUpdatePriority();
const prevTransition = ReactCurrentBatchConfig.transition;
try {
ReactCurrentBatchConfig.transition = 0;
setCurrentUpdatePriority(DiscreteEventPriority);
commitRootImpl(root, previousUpdateLanePriority);
commitRootImpl(root, recoverableErrors, previousUpdateLanePriority);
} finally {
ReactCurrentBatchConfig.transition = prevTransition;
setCurrentUpdatePriority(previousUpdateLanePriority);
@@ -1798,7 +1835,11 @@ function commitRoot(root) {
return null;
}

function commitRootImpl(root, renderPriorityLevel) {
function commitRootImpl(
root: FiberRoot,
recoverableErrors: null | Array<mixed>,
renderPriorityLevel: EventPriority,
) {
do {
// `flushPassiveEffects` will call `flushSyncUpdateQueue` at the end, which
// means `flushPassiveEffects` will sometimes result in additional
@@ -2069,6 +2110,22 @@ function commitRootImpl(root, renderPriorityLevel) {
// additional work on this root is scheduled.
ensureRootIsScheduled(root, now());

if (recoverableErrors !== null) {
// There were errors during this render, but recovered from them without
// needing to surface it to the UI. We log them here.
for (let i = 0; i < recoverableErrors.length; i++) {
const recoverableError = recoverableErrors[i];
const onRecoverableError = root.onRecoverableError;
if (onRecoverableError !== null) {
onRecoverableError(recoverableError);
} else {
// No user-provided onRecoverableError. Use the default behavior
// provided by the renderer's host config.
logRecoverableError(recoverableError);
}
}
}

if (hasUncaughtError) {
hasUncaughtError = false;
const error = firstUncaughtError;
79 changes: 68 additions & 11 deletions packages/react-reconciler/src/ReactFiberWorkLoop.old.js
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ import type {SuspenseState} from './ReactFiberSuspenseComponent.old';
import type {StackCursor} from './ReactFiberStack.old';
import type {Flags} from './ReactFiberFlags';
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.old';
import type {EventPriority} from './ReactEventPriorities.old';

import {
warnAboutDeprecatedLifecycles,
@@ -76,6 +77,7 @@ import {
supportsMicrotasks,
errorHydratingContainer,
scheduleMicrotask,
logRecoverableError,
} from './ReactFiberHostConfig';

import {
@@ -296,6 +298,11 @@ let workInProgressRootInterleavedUpdatedLanes: Lanes = NoLanes;
let workInProgressRootRenderPhaseUpdatedLanes: Lanes = NoLanes;
// Lanes that were pinged (in an interleaved event) during this render.
let workInProgressRootPingedLanes: Lanes = NoLanes;
// Errors that are thrown during the render phase.
let workInProgressRootConcurrentErrors: Array<mixed> | null = null;
// These are errors that we recovered from without surfacing them to the UI.
// We will log them once the tree commits.
let workInProgressRootRecoverableErrors: Array<mixed> | 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.
@@ -894,13 +901,36 @@ function recoverFromConcurrentError(root, errorRetryLanes) {
}
}

const errorsFromFirstAttempt = workInProgressRootConcurrentErrors;
const exitStatus = renderRootSync(root, errorRetryLanes);
if (exitStatus !== RootErrored) {
// Successfully finished rendering on retry
if (errorsFromFirstAttempt !== null) {
// The errors from the failed first attempt have been recovered. Add
// them to the collection of recoverable errors. We'll log them in the
// commit phase.
queueRecoverableErrors(errorsFromFirstAttempt);
}
} else {
// The UI failed to recover.
}

executionContext = prevExecutionContext;

return exitStatus;
}

export function queueRecoverableErrors(errors: Array<mixed>) {
if (workInProgressRootConcurrentErrors === null) {
workInProgressRootRecoverableErrors = errors;
} else {
workInProgressRootConcurrentErrors = workInProgressRootConcurrentErrors.push.apply(
null,
errors,
);
}
}

function finishConcurrentRender(root, exitStatus, lanes) {
switch (exitStatus) {
case RootIncomplete:
@@ -913,7 +943,7 @@ function finishConcurrentRender(root, exitStatus, lanes) {
case RootErrored: {
// We should have already attempted to retry this tree. If we reached
// this point, it errored again. Commit it.
commitRoot(root);
commitRoot(root, workInProgressRootRecoverableErrors);
break;
}
case RootSuspended: {
@@ -953,14 +983,14 @@ function finishConcurrentRender(root, exitStatus, lanes) {
// lower priority work to do. Instead of committing the fallback
// immediately, wait for more data to arrive.
root.timeoutHandle = scheduleTimeout(
commitRoot.bind(null, root),
commitRoot.bind(null, root, workInProgressRootRecoverableErrors),
msUntilTimeout,
);
break;
}
}
// The work expired. Commit immediately.
commitRoot(root);
commitRoot(root, workInProgressRootRecoverableErrors);
break;
}
case RootSuspendedWithDelay: {
@@ -991,20 +1021,20 @@ function finishConcurrentRender(root, exitStatus, lanes) {
// Instead of committing the fallback immediately, wait for more data
// to arrive.
root.timeoutHandle = scheduleTimeout(
commitRoot.bind(null, root),
commitRoot.bind(null, root, workInProgressRootRecoverableErrors),
msUntilTimeout,
);
break;
}
}

// Commit the placeholder.
commitRoot(root);
commitRoot(root, workInProgressRootRecoverableErrors);
break;
}
case RootCompleted: {
// The work completed. Ready to commit.
commitRoot(root);
commitRoot(root, workInProgressRootRecoverableErrors);
break;
}
default: {
@@ -1124,7 +1154,7 @@ function performSyncWorkOnRoot(root) {
const finishedWork: Fiber = (root.current.alternate: any);
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
commitRoot(root);
commitRoot(root, workInProgressRootRecoverableErrors);

// Before exiting, make sure there's a callback scheduled for the next
// pending level.
@@ -1320,6 +1350,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
workInProgressRootInterleavedUpdatedLanes = NoLanes;
workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
workInProgressRootPingedLanes = NoLanes;
workInProgressRootConcurrentErrors = null;
workInProgressRootRecoverableErrors = null;

enqueueInterleavedUpdates();

@@ -1474,10 +1506,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.
@@ -1781,15 +1818,15 @@ function completeUnitOfWork(unitOfWork: Fiber): void {
}
}

function commitRoot(root) {
function commitRoot(root: FiberRoot, recoverableErrors: null | Array<mixed>) {
// TODO: This no longer makes any sense. We already wrap the mutation and
// layout phases. Should be able to remove.
const previousUpdateLanePriority = getCurrentUpdatePriority();
const prevTransition = ReactCurrentBatchConfig.transition;
try {
ReactCurrentBatchConfig.transition = 0;
setCurrentUpdatePriority(DiscreteEventPriority);
commitRootImpl(root, previousUpdateLanePriority);
commitRootImpl(root, recoverableErrors, previousUpdateLanePriority);
} finally {
ReactCurrentBatchConfig.transition = prevTransition;
setCurrentUpdatePriority(previousUpdateLanePriority);
@@ -1798,7 +1835,11 @@ function commitRoot(root) {
return null;
}

function commitRootImpl(root, renderPriorityLevel) {
function commitRootImpl(
root: FiberRoot,
recoverableErrors: null | Array<mixed>,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is kind of annoying but there's no obvious place to store these. We don't usually pass things directly from the render phase to the commit phase because we stash it on a fiber.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get this part. All calls to commitRoot call this with the global workInProgressRootRecoverableErrors which you have access to here too. Other workInProgress globals are accessed here too.

So can't you just avoid passing it and instead access workInProgressRootRecoverableErrors here?

Copy link
Collaborator Author

@acdlite acdlite Feb 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an annoying edge case when both 1) you're in a multi-root app 2) the commit phase is delayed with setTimeout (this used to be more common but now it's really just the Suspense train model). During the timeout delay, the next root can start rendering. So you can't access work loop variables directly; you need to stash data somewhere in the fiber tree or on the FiberRoot.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, and with suspense images probably. That is annoying.

renderPriorityLevel: EventPriority,
) {
do {
// `flushPassiveEffects` will call `flushSyncUpdateQueue` at the end, which
// means `flushPassiveEffects` will sometimes result in additional
@@ -2069,6 +2110,22 @@ function commitRootImpl(root, renderPriorityLevel) {
// additional work on this root is scheduled.
ensureRootIsScheduled(root, now());

if (recoverableErrors !== null) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timing of these are interesting given that we were talking about logging order. If it's uncaught it'll be logged after this, but if it's caught the logging will happen in an error boundary who is supposed to log it in componentDidCatch which is a layout effect.

So it seems to me that this needs to be logged before the layout effect phase.

Copy link
Collaborator Author

@acdlite acdlite Feb 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point, though I don't know if we can be sure of the order unless we start tracking the recoverable errors per subtree. If there were recoverable errors and regular errors (componentDidCatch) in the same commit, the current implementation can't tell which happened first because the recoverable error array is shared across the whole tree.

Copy link
Collaborator

@sebmarkbage sebmarkbage Feb 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea but if they're separate subtrees they're more likely independent and we could've rendered them in any order.

The cases I'm mostly thinking about is when you have something like a hydration that succeeded and then errored because it wasn't quite fixed. However, I can't think of a case where that wouldn't be two separate consecutive commits.

That would be more of an issue with the throw vs next task throw thing, but not this case.

// There were errors during this render, but recovered from them without
// needing to surface it to the UI. We log them here.
for (let i = 0; i < recoverableErrors.length; i++) {
const recoverableError = recoverableErrors[i];
const onRecoverableError = root.onRecoverableError;
if (onRecoverableError !== null) {
onRecoverableError(recoverableError);
} else {
// No user-provided onRecoverableError. Use the default behavior
// provided by the renderer's host config.
logRecoverableError(recoverableError);
}
}
}

if (hasUncaughtError) {
hasUncaughtError = false;
const error = firstUncaughtError;
2 changes: 2 additions & 0 deletions packages/react-reconciler/src/ReactInternalTypes.js
Original file line number Diff line number Diff line change
@@ -246,6 +246,8 @@ type BaseFiberRootProperties = {|
// the public createRoot object, which the fiber tree does not currently have
// a reference to.
identifierPrefix: string,

onRecoverableError: null | ((error: mixed) => void),
|};

// The following attributes are only used by DevTools and are only present in DEV builds.
Original file line number Diff line number Diff line change
@@ -76,6 +76,7 @@ describe('ReactFiberHostContext', () => {
null,
false,
'',
null,
);
act(() => {
Renderer.updateContainer(
@@ -139,6 +140,7 @@ describe('ReactFiberHostContext', () => {
null,
false,
'',
null,
);
act(() => {
Renderer.updateContainer(
Original file line number Diff line number Diff line change
@@ -205,6 +205,9 @@ describe('useMutableSourceHydration', () => {
act(() => {
ReactDOM.hydrateRoot(container, <TestComponent />, {
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, <TestComponent />, {
mutableSources: [mutableSource],
onRecoverableError(error) {
Scheduler.unstable_yieldValue('Log error: ' + error.message);
},
});
});
} else {
ReactDOM.hydrateRoot(container, <TestComponent />, {
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 <div>.',
{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 <div>.',
{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
Original file line number Diff line number Diff line change
@@ -68,6 +68,7 @@ export const prepareScopeUpdate = $$$hostConfig.preparePortalMount;
export const getInstanceFromScope = $$$hostConfig.getInstanceFromScope;
export const getCurrentEventPriority = $$$hostConfig.getCurrentEventPriority;
export const detachDeletedInstance = $$$hostConfig.detachDeletedInstance;
export const logRecoverableError = $$$hostConfig.logRecoverableError;

// -------------------
// Microtasks
12 changes: 6 additions & 6 deletions packages/react-server/src/ReactFizzServer.js
Original file line number Diff line number Diff line change
@@ -401,7 +401,7 @@ function popComponentStackInDEV(task: Task): void {
}
}

function reportError(request: Request, error: mixed): void {
function logRecoverableError(request: Request, error: mixed): void {
// If this callback errors, we intentionally let that error bubble up to become a fatal error
// so that someone fixes the error reporting instead of hiding it.
const onError = request.onError;
@@ -484,7 +484,7 @@ function renderSuspenseBoundary(
}
} catch (error) {
contentRootSegment.status = ERRORED;
reportError(request, error);
logRecoverableError(request, error);
newBoundary.forceClientRender = true;
// We don't need to decrement any task numbers because we didn't spawn any new task.
// We don't need to schedule any task because we know the parent has written yet.
@@ -1337,7 +1337,7 @@ function erroredTask(
error: mixed,
) {
// Report the error to a global handler.
reportError(request, error);
logRecoverableError(request, error);
if (boundary === null) {
fatalError(request, error);
} else {
@@ -1557,7 +1557,7 @@ export function performWork(request: Request): void {
flushCompletedQueues(request, request.destination);
}
} catch (error) {
reportError(request, error);
logRecoverableError(request, error);
fatalError(request, error);
} finally {
setCurrentResponseState(prevResponseState);
@@ -1945,7 +1945,7 @@ export function startFlowing(request: Request, destination: Destination): void {
try {
flushCompletedQueues(request, destination);
} catch (error) {
reportError(request, error);
logRecoverableError(request, error);
fatalError(request, error);
}
}
@@ -1960,7 +1960,7 @@ export function abort(request: Request): void {
flushCompletedQueues(request, request.destination);
}
} catch (error) {
reportError(request, error);
logRecoverableError(request, error);
fatalError(request, error);
}
}
10 changes: 5 additions & 5 deletions packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
@@ -421,7 +421,7 @@ export function resolveModelToJSON(
x.then(ping, ping);
return serializeByRefID(newSegment.id);
} else {
reportError(request, x);
logRecoverableError(request, x);
// Something errored. We'll still send everything we have up until this point.
// We'll replace this element with a lazy reference that throws on the client
// once it gets rendered.
@@ -604,7 +604,7 @@ export function resolveModelToJSON(
);
}

function reportError(request: Request, error: mixed): void {
function logRecoverableError(request: Request, error: mixed): void {
const onError = request.onError;
onError(error);
}
@@ -687,7 +687,7 @@ function retrySegment(request: Request, segment: Segment): void {
x.then(ping, ping);
return;
} else {
reportError(request, x);
logRecoverableError(request, x);
// This errored, we need to serialize this error to the
emitErrorChunk(request, segment.id, x);
}
@@ -711,7 +711,7 @@ function performWork(request: Request): void {
flushCompletedChunks(request, request.destination);
}
} catch (error) {
reportError(request, error);
logRecoverableError(request, error);
fatalError(request, error);
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
@@ -794,7 +794,7 @@ export function startFlowing(request: Request, destination: Destination): void {
try {
flushCompletedChunks(request, destination);
} catch (error) {
reportError(request, error);
logRecoverableError(request, error);
fatalError(request, error);
}
}
4 changes: 4 additions & 0 deletions packages/react-test-renderer/src/ReactTestHostConfig.js
Original file line number Diff line number Diff line change
@@ -314,3 +314,7 @@ export function getInstanceFromScope(scopeInstance: Object): null | Object {
export function detachDeletedInstance(node: Instance): void {
// noop
}

export function logRecoverableError(error: mixed): void {
// noop
}
1 change: 1 addition & 0 deletions packages/react-test-renderer/src/ReactTestRenderer.js
Original file line number Diff line number Diff line change
@@ -472,6 +472,7 @@ function create(element: React$Element<any>, options: TestRendererOptions) {
isStrictMode,
concurrentUpdatesByDefault,
'',
null,
);

if (root == null) {
1 change: 1 addition & 0 deletions scripts/flow/environment.js
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@ declare var __REACT_DEVTOOLS_GLOBAL_HOOK__: any; /*?{
};*/

declare var queueMicrotask: (fn: Function) => void;
declare var reportError: (error: mixed) => void;

declare module 'create-react-class' {
declare var exports: React$CreateClass;
10 changes: 4 additions & 6 deletions scripts/print-warnings/print-warnings.js
Original file line number Diff line number Diff line change
@@ -67,12 +67,10 @@ function transform(file, enc, cb) {
const warningMsgLiteral = evalStringConcat(node.arguments[0]);
warnings.add(JSON.stringify(warningMsgLiteral));
} catch (error) {
console.error(
'Failed to extract warning message from',
file.path
);
console.error(astPath.node.loc);
throw error;
// Silently skip over this call. We have a lint rule to enforce
// that all calls are extractable, so if this one fails, assume
// it's intentional.
return;
}
}
},
1 change: 1 addition & 0 deletions scripts/rollup/validate/eslintrc.cjs.js
Original file line number Diff line number Diff line change
@@ -31,6 +31,7 @@ module.exports = {
ArrayBuffer: 'readonly',

TaskController: 'readonly',
reportError: 'readonly',

// Flight
Uint8Array: 'readonly',
1 change: 1 addition & 0 deletions scripts/rollup/validate/eslintrc.cjs2015.js
Original file line number Diff line number Diff line change
@@ -30,6 +30,7 @@ module.exports = {
ArrayBuffer: 'readonly',

TaskController: 'readonly',
reportError: 'readonly',

// Flight
Uint8Array: 'readonly',
1 change: 1 addition & 0 deletions scripts/rollup/validate/eslintrc.esm.js
Original file line number Diff line number Diff line change
@@ -30,6 +30,7 @@ module.exports = {
ArrayBuffer: 'readonly',

TaskController: 'readonly',
reportError: 'readonly',

// Flight
Uint8Array: 'readonly',
1 change: 1 addition & 0 deletions scripts/rollup/validate/eslintrc.fb.js
Original file line number Diff line number Diff line change
@@ -31,6 +31,7 @@ module.exports = {
ArrayBuffer: 'readonly',

TaskController: 'readonly',
reportError: 'readonly',

// Flight
Uint8Array: 'readonly',
1 change: 1 addition & 0 deletions scripts/rollup/validate/eslintrc.rn.js
Original file line number Diff line number Diff line change
@@ -31,6 +31,7 @@ module.exports = {
ArrayBuffer: 'readonly',

TaskController: 'readonly',
reportError: 'readonly',

// jest
jest: 'readonly',
1 change: 1 addition & 0 deletions scripts/rollup/validate/eslintrc.umd.js
Original file line number Diff line number Diff line change
@@ -36,6 +36,7 @@ module.exports = {
ArrayBuffer: 'readonly',

TaskController: 'readonly',
reportError: 'readonly',

// Flight
Uint8Array: 'readonly',