Skip to content

Commit f6d1df6

Browse files
authored
[Flight] erroring after abort should not result in unhandled rejection (#30675)
When I implemented the ability to abort synchronoulsy in flight I made it possible for erroring async server components to cause an unhandled rejection error. In the current implementation if you abort during the synchronous phase of a Function Component and then throw an error in the synchronous phase React will not attach any promise handlers because it short circuits the thenable treatment and throws an AbortSigil instead. This change updates the rendering logic to ignore the rejecting component.
1 parent a601d1d commit f6d1df6

File tree

2 files changed

+88
-12
lines changed

2 files changed

+88
-12
lines changed

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2485,4 +2485,73 @@ describe('ReactFlightDOM', () => {
24852485
</div>,
24862486
);
24872487
});
2488+
2489+
it('can error synchronously after aborting without an unhandled rejection error', async () => {
2490+
function App() {
2491+
return (
2492+
<div>
2493+
<Suspense fallback={<p>loading...</p>}>
2494+
<ComponentThatAborts />
2495+
</Suspense>
2496+
</div>
2497+
);
2498+
}
2499+
2500+
const abortRef = {current: null};
2501+
2502+
async function ComponentThatAborts() {
2503+
abortRef.current();
2504+
throw new Error('boom');
2505+
}
2506+
2507+
const {writable: flightWritable, readable: flightReadable} =
2508+
getTestStream();
2509+
2510+
await serverAct(() => {
2511+
const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
2512+
<App />,
2513+
webpackMap,
2514+
);
2515+
abortRef.current = abort;
2516+
pipe(flightWritable);
2517+
});
2518+
2519+
assertConsoleErrorDev([
2520+
'The render was aborted by the server without a reason.',
2521+
]);
2522+
2523+
const response =
2524+
ReactServerDOMClient.createFromReadableStream(flightReadable);
2525+
2526+
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
2527+
2528+
function ClientApp() {
2529+
return use(response);
2530+
}
2531+
2532+
const shellErrors = [];
2533+
await serverAct(async () => {
2534+
ReactDOMFizzServer.renderToPipeableStream(
2535+
React.createElement(ClientApp),
2536+
{
2537+
onShellError(error) {
2538+
shellErrors.push(error.message);
2539+
},
2540+
},
2541+
).pipe(fizzWritable);
2542+
});
2543+
assertConsoleErrorDev([
2544+
'The render was aborted by the server without a reason.',
2545+
]);
2546+
2547+
expect(shellErrors).toEqual([]);
2548+
2549+
const container = document.createElement('div');
2550+
await readInto(container, fizzReadable);
2551+
expect(getMeaningfulChildren(container)).toEqual(
2552+
<div>
2553+
<p>loading...</p>
2554+
</div>,
2555+
);
2556+
});
24882557
});

packages/react-server/src/ReactFlightServer.js

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -997,6 +997,8 @@ function callWithDebugContextInDEV<A, T>(
997997
}
998998
}
999999

1000+
const voidHandler = () => {};
1001+
10001002
function renderFunctionComponent<Props>(
10011003
request: Request,
10021004
task: Task,
@@ -1101,6 +1103,14 @@ function renderFunctionComponent<Props>(
11011103
}
11021104

11031105
if (request.status === ABORTING) {
1106+
if (
1107+
typeof result === 'object' &&
1108+
result !== null &&
1109+
typeof result.then === 'function' &&
1110+
!isClientReference(result)
1111+
) {
1112+
result.then(voidHandler, voidHandler);
1113+
}
11041114
// If we aborted during rendering we should interrupt the render but
11051115
// we don't need to provide an error because the renderer will encode
11061116
// the abort error as the reason.
@@ -1120,18 +1130,15 @@ function renderFunctionComponent<Props>(
11201130
// If the thenable resolves to an element, then it was in a static position,
11211131
// the return value of a Server Component. That doesn't need further validation
11221132
// of keys. The Server Component itself would have had a key.
1123-
thenable.then(
1124-
resolvedValue => {
1125-
if (
1126-
typeof resolvedValue === 'object' &&
1127-
resolvedValue !== null &&
1128-
resolvedValue.$$typeof === REACT_ELEMENT_TYPE
1129-
) {
1130-
resolvedValue._store.validated = 1;
1131-
}
1132-
},
1133-
() => {},
1134-
);
1133+
thenable.then(resolvedValue => {
1134+
if (
1135+
typeof resolvedValue === 'object' &&
1136+
resolvedValue !== null &&
1137+
resolvedValue.$$typeof === REACT_ELEMENT_TYPE
1138+
) {
1139+
resolvedValue._store.validated = 1;
1140+
}
1141+
}, voidHandler);
11351142
}
11361143
if (thenable.status === 'fulfilled') {
11371144
return thenable.value;

0 commit comments

Comments
 (0)