diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
index 77a2241734d3e..718ddf6c5716c 100644
--- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
@@ -2554,4 +2554,100 @@ describe('ReactFlightDOM', () => {
,
);
});
+
+ it('can error synchronously after aborting in a synchronous Component', async () => {
+ const rejectError = new Error('bam!');
+ const rejectedPromise = Promise.reject(rejectError);
+ rejectedPromise.catch(() => {});
+ rejectedPromise.status = 'rejected';
+ rejectedPromise.reason = rejectError;
+
+ const resolvedValue =
hello world
;
+ const resolvedPromise = Promise.resolve(resolvedValue);
+ resolvedPromise.status = 'fulfilled';
+ resolvedPromise.value = resolvedValue;
+
+ function App() {
+ return (
+
+ loading...}>
+
+
+ loading too...}>
+ {rejectedPromise}
+
+ loading three...}>
+ {resolvedPromise}
+
+
+ );
+ }
+
+ const abortRef = {current: null};
+
+ // This test is specifically asserting that this works with Sync Server Component
+ function ComponentThatAborts() {
+ abortRef.current();
+ throw new Error('boom');
+ }
+
+ const {writable: flightWritable, readable: flightReadable} =
+ getTestStream();
+
+ await serverAct(() => {
+ const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
+ ,
+ webpackMap,
+ {
+ onError(e) {
+ console.error(e);
+ },
+ },
+ );
+ abortRef.current = abort;
+ pipe(flightWritable);
+ });
+
+ assertConsoleErrorDev([
+ 'The render was aborted by the server without a reason.',
+ 'bam!',
+ ]);
+
+ const response =
+ ReactServerDOMClient.createFromReadableStream(flightReadable);
+
+ const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
+
+ function ClientApp() {
+ return use(response);
+ }
+
+ const shellErrors = [];
+ await serverAct(async () => {
+ ReactDOMFizzServer.renderToPipeableStream(
+ React.createElement(ClientApp),
+ {
+ onShellError(error) {
+ shellErrors.push(error.message);
+ },
+ },
+ ).pipe(fizzWritable);
+ });
+ assertConsoleErrorDev([
+ 'The render was aborted by the server without a reason.',
+ 'bam!',
+ ]);
+
+ expect(shellErrors).toEqual([]);
+
+ const container = document.createElement('div');
+ await readInto(container, fizzReadable);
+ expect(getMeaningfulChildren(container)).toEqual(
+
+
loading...
+
loading too...
+
hello world
+
,
+ );
+ });
});
diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js
index 10ceee8075bd3..6c9536d95acf7 100644
--- a/packages/react-server/src/ReactFlightServer.js
+++ b/packages/react-server/src/ReactFlightServer.js
@@ -382,8 +382,6 @@ export type Request = {
didWarnForKey: null | WeakSet,
};
-const AbortSigil = {};
-
const {
TaintRegistryObjects,
TaintRegistryValues,
@@ -594,6 +592,8 @@ function serializeThenable(
const digest = logRecoverableError(request, x, null);
emitErrorChunk(request, newTask.id, digest, x);
}
+ newTask.status = ERRORED;
+ request.abortableTasks.delete(newTask);
return newTask.id;
}
default: {
@@ -650,10 +650,10 @@ function serializeThenable(
logPostpone(request, postponeInstance.message, newTask);
emitPostponeChunk(request, newTask.id, postponeInstance);
} else {
- newTask.status = ERRORED;
const digest = logRecoverableError(request, reason, newTask);
emitErrorChunk(request, newTask.id, digest, reason);
}
+ newTask.status = ERRORED;
request.abortableTasks.delete(newTask);
enqueueFlush(request);
},
@@ -1114,7 +1114,8 @@ function renderFunctionComponent(
// If we aborted during rendering we should interrupt the render but
// we don't need to provide an error because the renderer will encode
// the abort error as the reason.
- throw AbortSigil;
+ // eslint-disable-next-line no-throw-literal
+ throw null;
}
if (
@@ -1616,7 +1617,8 @@ function renderElement(
// lazy initializers are user code and could abort during render
// we don't wan to return any value resolved from the lazy initializer
// if it aborts so we interrupt rendering here
- throw AbortSigil;
+ // eslint-disable-next-line no-throw-literal
+ throw null;
}
return renderElement(
request,
@@ -2183,7 +2185,7 @@ function renderModel(
}
}
- if (thrownValue === AbortSigil) {
+ if (request.status === ABORTING) {
task.status = ABORTED;
const errorId: number = (request.fatalError: any);
if (wasReactNode) {
@@ -2357,7 +2359,8 @@ function renderModelDestructive(
// lazy initializers are user code and could abort during render
// we don't wan to return any value resolved from the lazy initializer
// if it aborts so we interrupt rendering here
- throw AbortSigil;
+ // eslint-disable-next-line no-throw-literal
+ throw null;
}
if (__DEV__) {
const debugInfo: ?ReactDebugInfo = lazy._debugInfo;
@@ -3690,7 +3693,7 @@ function retryTask(request: Request, task: Task): void {
}
}
- if (x === AbortSigil) {
+ if (request.status === ABORTING) {
request.abortableTasks.delete(task);
task.status = ABORTED;
const errorId: number = (request.fatalError: any);
@@ -3909,7 +3912,9 @@ export function stopFlowing(request: Request): void {
// This is called to early terminate a request. It creates an error at all pending tasks.
export function abort(request: Request, reason: mixed): void {
try {
- request.status = ABORTING;
+ if (request.status === OPEN) {
+ request.status = ABORTING;
+ }
const abortableTasks = request.abortableTasks;
// We have tasks to abort. We'll emit one error row and then emit a reference
// to that row from every row that's still remaining.