Skip to content

Commit ca8f91f

Browse files
authored
Log errors from startViewTransition to onRecoverableError (#32540)
We customize the messages only in DEV to keep it small in prod. We skip some messages that are not really errors but more like information.
1 parent 2398554 commit ca8f91f

File tree

5 files changed

+109
-20
lines changed

5 files changed

+109
-20
lines changed

packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js

+91-20
Original file line numberDiff line numberDiff line change
@@ -1576,6 +1576,69 @@ function cancelAllViewTransitionAnimations(scope: Element) {
15761576
// either cached the font or preloaded it earlier.
15771577
const SUSPENSEY_FONT_TIMEOUT = 500;
15781578

1579+
function customizeViewTransitionError(error: Object): mixed {
1580+
if (typeof error === 'object' && error !== null) {
1581+
switch (error.name) {
1582+
case 'TimeoutError': {
1583+
// We assume that the only reason a Timeout can happen is because the Navigation
1584+
// promise. We expect any other work to either be fast or have a timeout (fonts).
1585+
if (__DEV__) {
1586+
// eslint-disable-next-line react-internal/prod-error-codes
1587+
return new Error(
1588+
'A ViewTransition timed out because a Navigation stalled. ' +
1589+
'This can happen if a Navigation is blocked on React itself. ' +
1590+
"Such as if it's resolved inside useEffect. " +
1591+
'This can be solved by moving the resolution to useLayoutEffect.',
1592+
{cause: error},
1593+
);
1594+
}
1595+
break;
1596+
}
1597+
case 'AbortError': {
1598+
if (__DEV__) {
1599+
// eslint-disable-next-line react-internal/prod-error-codes
1600+
return new Error(
1601+
'A ViewTransition was aborted early. This might be because you have ' +
1602+
'other View Transition libraries on the page and only one can run at ' +
1603+
"a time. To avoid this, use only React's built-in <ViewTransition> " +
1604+
'to coordinate.',
1605+
{cause: error},
1606+
);
1607+
}
1608+
break;
1609+
}
1610+
case 'InvalidStateError': {
1611+
if (
1612+
error.message ===
1613+
'View transition was skipped because document visibility state is hidden.' ||
1614+
error.message ===
1615+
'Skipping view transition because document visibility state has become hidden.' ||
1616+
error.message ===
1617+
'Skipping view transition because viewport size changed.'
1618+
) {
1619+
// Skip logging this. This is not considered an error.
1620+
return null;
1621+
}
1622+
if (__DEV__) {
1623+
if (
1624+
error.message === 'Transition was aborted because of invalid state'
1625+
) {
1626+
// Chrome doesn't include the reason in the message but logs it in the console..
1627+
// Redirect the user to look there.
1628+
// eslint-disable-next-line react-internal/prod-error-codes
1629+
return new Error(
1630+
'A ViewTransition could not start. See the console for more details.',
1631+
{cause: error},
1632+
);
1633+
}
1634+
}
1635+
break;
1636+
}
1637+
}
1638+
}
1639+
return error;
1640+
}
1641+
15791642
export function startViewTransition(
15801643
rootContainer: Container,
15811644
transitionTypes: null | TransitionTypes,
@@ -1584,6 +1647,7 @@ export function startViewTransition(
15841647
afterMutationCallback: () => void,
15851648
spawnedWorkCallback: () => void,
15861649
passiveCallback: () => mixed,
1650+
errorCallback: mixed => void,
15871651
): boolean {
15881652
const ownerDocument: Document =
15891653
rootContainer.nodeType === DOCUMENT_NODE
@@ -1641,24 +1705,19 @@ export function startViewTransition(
16411705
});
16421706
// $FlowFixMe[prop-missing]
16431707
ownerDocument.__reactViewTransition = transition;
1644-
if (__DEV__) {
1645-
transition.ready.then(undefined, (reason: mixed) => {
1646-
if (
1647-
typeof reason === 'object' &&
1648-
reason !== null &&
1649-
reason.name === 'TimeoutError'
1650-
) {
1651-
console.error(
1652-
'A ViewTransition timed out because a Navigation stalled. ' +
1653-
'This can happen if a Navigation is blocked on React itself. ' +
1654-
"Such as if it's resolved inside useEffect. " +
1655-
'This can be solved by moving the resolution to useLayoutEffect.',
1656-
);
1708+
const handleError = (error: mixed) => {
1709+
try {
1710+
error = customizeViewTransitionError(error);
1711+
if (error !== null) {
1712+
errorCallback(error);
16571713
}
1658-
});
1659-
}
1660-
transition.ready.then(spawnedWorkCallback, spawnedWorkCallback);
1661-
transition.finished.then(() => {
1714+
} finally {
1715+
// Continue the reset of the work.
1716+
spawnedWorkCallback();
1717+
}
1718+
};
1719+
transition.ready.then(spawnedWorkCallback, handleError);
1720+
transition.finished.finally(() => {
16621721
cancelAllViewTransitionAnimations((ownerDocument.documentElement: any));
16631722
// $FlowFixMe[prop-missing]
16641723
if (ownerDocument.__reactViewTransition === transition) {
@@ -1802,6 +1861,7 @@ export function startGestureTransition(
18021861
transitionTypes: null | TransitionTypes,
18031862
mutationCallback: () => void,
18041863
animateCallback: () => void,
1864+
errorCallback: mixed => void,
18051865
): null | RunningGestureTransition {
18061866
const ownerDocument: Document =
18071867
rootContainer.nodeType === DOCUMENT_NODE
@@ -1815,7 +1875,7 @@ export function startGestureTransition(
18151875
});
18161876
// $FlowFixMe[prop-missing]
18171877
ownerDocument.__reactViewTransition = transition;
1818-
const readyCallback = (x: any) => {
1878+
const readyCallback = () => {
18191879
const documentElement: Element = (ownerDocument.documentElement: any);
18201880
// Loop through all View Transition Animations.
18211881
const animations = documentElement.getAnimations({subtree: true});
@@ -1935,8 +1995,19 @@ export function startGestureTransition(
19351995
navigator.userAgent.indexOf('Chrome') !== -1
19361996
? () => requestAnimationFrame(readyCallback)
19371997
: readyCallback;
1938-
transition.ready.then(readyForAnimations, readyCallback);
1939-
transition.finished.then(() => {
1998+
const handleError = (error: mixed) => {
1999+
try {
2000+
error = customizeViewTransitionError(error);
2001+
if (error !== null) {
2002+
errorCallback(error);
2003+
}
2004+
} finally {
2005+
// Continue the reset of the work.
2006+
readyCallback();
2007+
}
2008+
};
2009+
transition.ready.then(readyForAnimations, handleError);
2010+
transition.finished.finally(() => {
19402011
cancelAllViewTransitionAnimations((ownerDocument.documentElement: any));
19412012
// $FlowFixMe[prop-missing]
19422013
if (ownerDocument.__reactViewTransition === transition) {

packages/react-native-renderer/src/ReactFiberConfigNative.js

+2
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,7 @@ export function startViewTransition(
619619
afterMutationCallback: () => void,
620620
spawnedWorkCallback: () => void,
621621
passiveCallback: () => mixed,
622+
errorCallback: mixed => void,
622623
): boolean {
623624
return false;
624625
}
@@ -633,6 +634,7 @@ export function startGestureTransition(
633634
transitionTypes: null | TransitionTypes,
634635
mutationCallback: () => void,
635636
animateCallback: () => void,
637+
errorCallback: mixed => void,
636638
): RunningGestureTransition {
637639
mutationCallback();
638640
animateCallback();

packages/react-noop-renderer/src/createReactNoop.js

+2
Original file line numberDiff line numberDiff line change
@@ -809,6 +809,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
809809
afterMutationCallback: () => void,
810810
layoutCallback: () => void,
811811
passiveCallback: () => mixed,
812+
errorCallback: mixed => void,
812813
): boolean {
813814
return false;
814815
},
@@ -821,6 +822,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
821822
transitionTypes: null | TransitionTypes,
822823
mutationCallback: () => void,
823824
animateCallback: () => void,
825+
errorCallback: mixed => void,
824826
): RunningGestureTransition {
825827
mutationCallback();
826828
animateCallback();

packages/react-reconciler/src/ReactFiberWorkLoop.js

+12
Original file line numberDiff line numberDiff line change
@@ -3511,6 +3511,7 @@ function commitRoot(
35113511
flushAfterMutationEffects,
35123512
flushSpawnedWork,
35133513
flushPassiveEffects,
3514+
reportViewTransitionError,
35143515
);
35153516
if (!startedViewTransition) {
35163517
// Flush synchronously.
@@ -3521,6 +3522,16 @@ function commitRoot(
35213522
}
35223523
}
35233524

3525+
function reportViewTransitionError(error: mixed) {
3526+
// Report errors that happens while preparing a View Transition.
3527+
if (pendingEffectsStatus === NO_PENDING_EFFECTS) {
3528+
return;
3529+
}
3530+
const root = pendingEffectsRoot;
3531+
const onRecoverableError = root.onRecoverableError;
3532+
onRecoverableError(error, makeErrorInfo(null));
3533+
}
3534+
35243535
function flushAfterMutationEffects(): void {
35253536
if (pendingEffectsStatus !== PENDING_AFTER_MUTATION_PHASE) {
35263537
return;
@@ -3911,6 +3922,7 @@ function commitGestureOnRoot(
39113922
pendingTransitionTypes,
39123923
flushGestureMutations,
39133924
flushGestureAnimations,
3925+
reportViewTransitionError,
39143926
);
39153927
}
39163928

packages/react-test-renderer/src/ReactFiberConfigTestHost.js

+2
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,7 @@ export function startViewTransition(
417417
afterMutationCallback: () => void,
418418
spawnedWorkCallback: () => void,
419419
passiveCallback: () => mixed,
420+
errorCallback: mixed => void,
420421
): boolean {
421422
return false;
422423
}
@@ -431,6 +432,7 @@ export function startGestureTransition(
431432
transitionTypes: null | TransitionTypes,
432433
mutationCallback: () => void,
433434
animateCallback: () => void,
435+
errorCallback: mixed => void,
434436
): RunningGestureTransition {
435437
mutationCallback();
436438
animateCallback();

0 commit comments

Comments
 (0)