Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit c1bff89

Browse files
committedJun 10, 2024·
Encode Errors during serialization as Lazy at the Nearest Element
We feel comfortable turning any Element into Lazy since it serializes as Node. So if any error happens inside of the deserialization such as if a direct reference errored or a client reference failed to load we can scope it to that element. That way if any Error boundaries were on the stack inside the same model, they can handle the error. This also gives us better debug info for things like serialization errors because they now can get a stack trace pointing to the exact JSX.
1 parent c0fe18d commit c1bff89

File tree

3 files changed

+124
-13
lines changed

3 files changed

+124
-13
lines changed
 

‎packages/react-client/src/ReactFlightClient.js

+73-3
Original file line numberDiff line numberDiff line change
@@ -732,8 +732,32 @@ function createElement(
732732
// This is effectively the complete phase.
733733
initializingHandler = handler.parent;
734734
if (handler.errored) {
735-
// TODO: Encode the error as Lazy.
736-
throw handler.value;
735+
// Something errored inside this Element's props. We can turn this Element
736+
// into a Lazy so that we can still render up until that Lazy is rendered.
737+
const erroredChunk: ErroredChunk<React$Element<any>> = createErrorChunk(
738+
response,
739+
handler.value,
740+
);
741+
if (__DEV__) {
742+
// Conceptually the error happened inside this Element but right before
743+
// it was rendered. We don't have a client side component to render but
744+
// we can add some DebugInfo to explain that this was conceptually a
745+
// Server side error that errored inside this element. That way any stack
746+
// traces will point to the nearest JSX that errored - e.g. during
747+
// serialization.
748+
const erroredComponent: ReactComponentInfo = {
749+
name: getComponentNameFromType(element.type) || '',
750+
owner: element._owner,
751+
};
752+
if (enableOwnerStacks) {
753+
// $FlowFixMe[cannot-write]
754+
erroredComponent.stack = element._debugStack;
755+
// $FlowFixMe[cannot-write]
756+
erroredComponent.task = element._debugTask;
757+
}
758+
erroredChunk._debugInfo = [erroredComponent];
759+
}
760+
return createLazyChunkWrapper(erroredChunk);
737761
}
738762
if (handler.deps > 0) {
739763
// We have blocked references inside this Element but we can turn this into
@@ -861,12 +885,43 @@ function waitForReference<T>(
861885
// Promise.all.
862886
return;
863887
}
888+
const blockedValue = handler.value;
864889
handler.errored = true;
865890
handler.value = error;
866891
const chunk = handler.chunk;
867892
if (chunk === null || chunk.status !== BLOCKED) {
868893
return;
869894
}
895+
896+
if (__DEV__) {
897+
if (
898+
typeof blockedValue === 'object' &&
899+
blockedValue !== null &&
900+
blockedValue.$$typeof === REACT_ELEMENT_TYPE
901+
) {
902+
const element = blockedValue;
903+
// Conceptually the error happened inside this Element but right before
904+
// it was rendered. We don't have a client side component to render but
905+
// we can add some DebugInfo to explain that this was conceptually a
906+
// Server side error that errored inside this element. That way any stack
907+
// traces will point to the nearest JSX that errored - e.g. during
908+
// serialization.
909+
const erroredComponent: ReactComponentInfo = {
910+
name: getComponentNameFromType(element.type) || '',
911+
owner: element._owner,
912+
};
913+
if (enableOwnerStacks) {
914+
// $FlowFixMe[cannot-write]
915+
erroredComponent.stack = element._debugStack;
916+
// $FlowFixMe[cannot-write]
917+
erroredComponent.task = element._debugTask;
918+
}
919+
const chunkDebugInfo: ReactDebugInfo =
920+
chunk._debugInfo || (chunk._debugInfo = []);
921+
chunkDebugInfo.push(erroredComponent);
922+
}
923+
}
924+
870925
triggerErrorOnChunk(chunk, error);
871926
}
872927

@@ -961,7 +1016,22 @@ function getOutlinedModel<T>(
9611016
case BLOCKED:
9621017
return waitForReference(chunk, parentObject, key, response, map, path);
9631018
default:
964-
throw chunk.reason;
1019+
// This is an error. Instead of erroring directly, we're going to encode this on
1020+
// an initialization handler so that we can catch it at the nearest Element.
1021+
if (initializingHandler) {
1022+
initializingHandler.errored = true;
1023+
initializingHandler.value = chunk.reason;
1024+
} else {
1025+
initializingHandler = {
1026+
parent: null,
1027+
chunk: null,
1028+
value: chunk.reason,
1029+
deps: 0,
1030+
errored: true,
1031+
};
1032+
}
1033+
// Placeholder
1034+
return (null: any);
9651035
}
9661036
}
9671037

‎packages/react-client/src/__tests__/ReactFlight-test.js

+40
Original file line numberDiff line numberDiff line change
@@ -1104,6 +1104,46 @@ describe('ReactFlight', () => {
11041104
});
11051105
});
11061106

1107+
it('should handle serialization errors in element inside error boundary', async () => {
1108+
const ClientErrorBoundary = clientReference(ErrorBoundary);
1109+
1110+
const expectedStack = __DEV__
1111+
? '\n in div' + '\n in ErrorBoundary (at **)' + '\n in App'
1112+
: '\n in ErrorBoundary (at **)';
1113+
1114+
function App() {
1115+
return (
1116+
<ClientErrorBoundary
1117+
expectedMessage="Event handlers cannot be passed to Client Component props."
1118+
expectedStack={expectedStack}>
1119+
<div onClick={function () {}} />
1120+
</ClientErrorBoundary>
1121+
);
1122+
}
1123+
1124+
const transport = ReactNoopFlightServer.render(<App />, {
1125+
onError(x) {
1126+
if (__DEV__) {
1127+
return 'a dev digest';
1128+
}
1129+
if (x instanceof Error) {
1130+
return `digest("${x.message}")`;
1131+
} else if (Array.isArray(x)) {
1132+
return `digest([])`;
1133+
} else if (typeof x === 'object' && x !== null) {
1134+
return `digest({})`;
1135+
}
1136+
return `digest(${String(x)})`;
1137+
},
1138+
});
1139+
1140+
await act(() => {
1141+
startTransition(() => {
1142+
ReactNoop.render(ReactNoopFlightClient.read(transport));
1143+
});
1144+
});
1145+
});
1146+
11071147
it('should include server components in warning stacks', async () => {
11081148
function Component() {
11091149
// Trigger key warning

‎packages/react-server/src/ReactFlightServer.js

+11-10
Original file line numberDiff line numberDiff line change
@@ -1039,7 +1039,9 @@ function renderFunctionComponent<Props>(
10391039
owner: owner,
10401040
};
10411041
if (enableOwnerStacks) {
1042-
(componentDebugInfo: any).stack = stack;
1042+
// $FlowFixMe[prop-missing]
1043+
// $FlowFixMe[cannot-write]
1044+
componentDebugInfo.stack = stack;
10431045
}
10441046
// We outline this model eagerly so that we can refer to by reference as an owner.
10451047
// If we had a smarter way to dedupe we might not have to do this if there ends up
@@ -2055,20 +2057,19 @@ function renderModel(
20552057
task.keyPath = prevKeyPath;
20562058
task.implicitSlot = prevImplicitSlot;
20572059

2060+
// Something errored. We'll still send everything we have up until this point.
2061+
request.pendingChunks++;
2062+
const errorId = request.nextChunkId++;
2063+
const digest = logRecoverableError(request, x);
2064+
emitErrorChunk(request, errorId, digest, x);
20582065
if (wasReactNode) {
2059-
// Something errored. We'll still send everything we have up until this point.
20602066
// We'll replace this element with a lazy reference that throws on the client
20612067
// once it gets rendered.
2062-
request.pendingChunks++;
2063-
const errorId = request.nextChunkId++;
2064-
const digest = logRecoverableError(request, x);
2065-
emitErrorChunk(request, errorId, digest, x);
20662068
return serializeLazyID(errorId);
20672069
}
2068-
// Something errored but it was not in a React Node. There's no need to serialize
2069-
// it by value because it'll just error the whole parent row anyway so we can
2070-
// just stop any siblings and error the whole parent row.
2071-
throw x;
2070+
// If we don't know if it was a React Node we render a direct reference and let
2071+
// the client deal with it.
2072+
return serializeByValueID(errorId);
20722073
}
20732074
}
20742075

0 commit comments

Comments
 (0)
Please sign in to comment.