@samitouri / QOS-React / commits / 3c882b4ab6

[Fizz] Finish abort in a scheduled task (#36580)

Stacked on #36584 `abort()` currently performs both the synchronous transition into an aborted request and the reporting/completion of every unfinished task in the same call. This change splits those phases. Aborting now synchronously marks the request as aborted, captures the abort reason, claims pending tasks so already scheduled work cannot continue rendering them, and captures any DEV async debug information needed at the point of abort. Reporting and completing the claimed tasks is then performed from a scheduled `finishAbort()` callback. This split does not yet allow a promise rejected by an abort listener to replace the abort reason: work remains blocked once the request has been aborted, and tests assert that abort-time rejections still report the original abort reason. It establishes the task boundary needed for a follow-up change to selectively process rejected suspended work before completing the remaining aborted tasks. This is observable for streaming renders because abort cleanup may now happen after already available output is read. A Suspense boundary that was previously converted to client rendering before it could be serialized may instead be emitted as pending first and receive its client-render instruction when the scheduled abort completion runs. The scheduled finish must also preserve abort-during-render behavior in renderers whose scheduler executes synchronously. The request tracks its currently executing task, and both abort phases leave that task alone so it can unwind through its normal abort path rather than being completed twice or reporting an internal control-flow value.

Josh Story committed May 31, 2026 at 23:32 UTC 3c882b4ab64cbbd2b2ad3b75f75afdedd7c83c00
7 files changed +296 -66
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+33 -5
@@ -7100,7 +7100,7 @@ describe('ReactDOMFizzServer', () => {
7100 expect(errors).toEqual(['abort reason', 'abort reason']);
7101 });
7102
7103 - it('reports a root task that suspends after aborting during render', async () => {
7103 + it('reports a root task before rendering a suspended child returned after aborting', async () => {
7104 const promise = new Promise(() => {});
7105 function SuspendedRoot() {
7106 use(promise);
@@ -7138,6 +7138,34 @@ describe('ReactDOMFizzServer', () => {
7138 expect(errors).toEqual(['abort reason', 'abort reason']);
7139 });
7140
7141 + it('currently does not report a root task that suspends directly after aborting during render', async () => {
7142 + const promise = new Promise(() => {});
7143 + const abortRef = {current: null};
7144 + function ComponentThatAbortsAndSuspends() {
7145 + abortRef.current(new Error('abort reason'));
7146 + use(promise);
7147 + return null;
7148 + }
7149 +
7150 + const errors = [];
7151 + await act(() => {
7152 + const {abort} = renderToPipeableStream(
7153 + <ComponentThatAbortsAndSuspends />,
7154 + {
7155 + onError(error) {
7156 + errors.push(error.message);
7157 + },
7158 + onShellError() {},
7159 + },
7160 + );
7161 + abortRef.current = abort;
7162 + });
7163 +
7164 + // The task suspends before renderFunctionComponent gets to throw the
7165 + // abort reason, and retryRenderTask currently suspends it again.
7166 + expect(errors).toEqual([]);
7167 + });
7168 +
7169 it('can abort during render in a lazy initializer for a component', async () => {
7170 function Sibling() {
7171 return <p>sibling</p>;
@@ -8443,6 +8471,10 @@ describe('ReactDOMFizzServer', () => {
8471
8472 expect(thrownError).toBe('boom');
8473 expect(errors).toEqual([
8474 + {
8475 + error: 'boom',
8476 + componentStack: componentStack(['Abort', 'body', 'html', 'App']),
8477 + },
8478 {
8479 error: 'boom',
8480 componentStack: componentStack([
@@ -8462,10 +8494,6 @@ describe('ReactDOMFizzServer', () => {
8494 'App',
8495 ]),
8496 },
8465 - {
8466 - error: 'boom',
8467 - componentStack: componentStack(['Abort', 'body', 'html', 'App']),
8468 - },
8497 ]);
8498
8499 // We expect the render to throw before streaming anything so the default
packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js
+13 -5
@@ -221,7 +221,9 @@ describe('ReactDOMFizzServerBrowser', () => {
221 ),
222 );
223
224 - controller.abort();
224 + await serverAct(() => {
225 + controller.abort();
226 + });
227
228 const result = await readResult(stream);
229 expect(result).toContain('Loading');
@@ -247,7 +249,9 @@ describe('ReactDOMFizzServerBrowser', () => {
249 );
250
251 const theReason = new Error('aborted for reasons');
250 - controller.abort(theReason);
252 + await serverAct(() => {
253 + controller.abort(theReason);
254 + });
255
256 let caughtError = null;
257 try {
@@ -364,7 +368,7 @@ describe('ReactDOMFizzServerBrowser', () => {
368
369 const reader = stream.getReader();
370 await reader.read();
367 - await reader.cancel();
371 + await serverAct(() => reader.cancel());
372
373 expect(errors).toEqual([
374 'The render was aborted by the server without a reason.',
@@ -463,7 +467,9 @@ describe('ReactDOMFizzServerBrowser', () => {
467 }),
468 );
469
466 - controller.abort('foobar');
470 + await serverAct(() => {
471 + controller.abort('foobar');
472 + });
473
474 expect(errors).toEqual(['foobar', 'foobar']);
475 });
@@ -502,7 +508,9 @@ describe('ReactDOMFizzServerBrowser', () => {
508 }),
509 );
510
505 - controller.abort(new Error('uh oh'));
511 + await serverAct(() => {
512 + controller.abort(new Error('uh oh'));
513 + });
514
515 expect(errors).toEqual(['uh oh', 'uh oh']);
516 });
packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js
+4
@@ -382,6 +382,7 @@ describe('ReactDOMFizzServerNode', () => {
382 expect(isCompleteCalls).toBe(0);
383
384 abort(new Error('uh oh'));
385 + await jest.runAllTimers();
386
387 await completed;
388
@@ -468,6 +469,7 @@ describe('ReactDOMFizzServerNode', () => {
469
470 const reason = new Error('abort reason');
471 abort(reason);
472 + await jest.runAllTimers();
473
474 expect(shellErrors).toEqual([reason]);
475 expect(errors).toEqual(['abort reason', 'abort reason', 'abort reason']);
@@ -504,6 +506,7 @@ describe('ReactDOMFizzServerNode', () => {
506 expect(isCompleteCalls).toBe(0);
507
508 abort();
509 + await jest.runAllTimers();
510
511 await completed;
512
@@ -746,6 +749,7 @@ describe('ReactDOMFizzServerNode', () => {
749 resolve();
750
751 await completed;
752 + await jest.runAllTimers();
753
754 expect(errors).toEqual([
755 'The destination stream errored while writing data.',
packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
+90 -14
@@ -299,7 +299,9 @@ describe('ReactDOMFizzStaticBrowser', () => {
299 );
300 });
301
302 - controller.abort();
302 + await serverAct(() => {
303 + controller.abort();
304 + });
305
306 const result = await resultPromise;
307
@@ -329,7 +331,9 @@ describe('ReactDOMFizzStaticBrowser', () => {
331 await jest.runAllTimers();
332
333 const theReason = new Error('aborted for reasons');
332 - controller.abort(theReason);
334 + await serverAct(() => {
335 + controller.abort(theReason);
336 + });
337
338 let rejected = false;
339 let prelude;
@@ -447,7 +451,9 @@ describe('ReactDOMFizzStaticBrowser', () => {
451 });
452 });
453
450 - controller.abort('foobar');
454 + await serverAct(() => {
455 + controller.abort('foobar');
456 + });
457
458 await resultPromise;
459
@@ -489,13 +495,63 @@ describe('ReactDOMFizzStaticBrowser', () => {
495 });
496 });
497
492 - controller.abort(new Error('uh oh'));
498 + await serverAct(() => {
499 + controller.abort(new Error('uh oh'));
500 + });
501
502 await resultPromise;
503
504 expect(errors).toEqual(['uh oh', 'uh oh']);
505 });
506
507 + it('currently uses the abort reason when an abort listener synchronously rejects pending work', async () => {
508 + let reject;
509 + const rejectedPromise = new Promise((resolve, rejectPromise) => {
510 + reject = rejectPromise;
511 + });
512 + const haltedPromise = new Promise(() => {});
513 + function RejectedWait() {
514 + React.use(rejectedPromise);
515 + return null;
516 + }
517 + function HaltedWait() {
518 + React.use(haltedPromise);
519 + return null;
520 + }
521 +
522 + const errors = [];
523 + const controller = new AbortController();
524 + let resultPromise;
525 + await serverAct(() => {
526 + resultPromise = ReactDOMFizzStatic.prerender(
527 + <>
528 + <Suspense fallback="Loading rejected">
529 + <RejectedWait />
530 + </Suspense>
531 + <Suspense fallback="Loading halted">
532 + <HaltedWait />
533 + </Suspense>
534 + </>,
535 + {
536 + signal: controller.signal,
537 + onError(error) {
538 + errors.push(error.message);
539 + },
540 + },
541 + );
542 + });
543 +
544 + controller.signal.addEventListener('abort', () => {
545 + reject(new Error('rejected during abort'));
546 + });
547 + await serverAct(() => {
548 + controller.abort(new Error('abort reason'));
549 + });
550 + await resultPromise;
551 +
552 + expect(errors).toEqual(['abort reason', 'abort reason']);
553 + });
554 +
555 it('logs an error if onHeaders throws but continues the prerender', async () => {
556 const errors = [];
557 function onError(error) {
@@ -562,7 +618,9 @@ describe('ReactDOMFizzStaticBrowser', () => {
618 });
619 });
620
565 - controller.abort();
621 + await serverAct(() => {
622 + controller.abort();
623 + });
624 const prerendered = await pendingResult;
625 const postponedState = JSON.stringify(prerendered.postponed);
626
@@ -587,7 +645,9 @@ describe('ReactDOMFizzStaticBrowser', () => {
645 );
646 });
647
590 - controller2.abort();
648 + await serverAct(() => {
649 + controller2.abort();
650 + });
651
652 const prerendered2 = await pendingResult;
653 const postponedState2 = JSON.stringify(prerendered2.postponed);
@@ -641,7 +701,9 @@ describe('ReactDOMFizzStaticBrowser', () => {
701 onError() {},
702 });
703 });
644 - controller.abort('prerender abort');
704 + await serverAct(() => {
705 + controller.abort('prerender abort');
706 + });
707 const prerendered = await pendingResult;
708
709 prerendering = false;
@@ -693,7 +755,9 @@ describe('ReactDOMFizzStaticBrowser', () => {
755 onError() {},
756 });
757 });
696 - controller.abort('prerender abort');
758 + await serverAct(() => {
759 + controller.abort('prerender abort');
760 + });
761 const prerendered = await pendingResult;
762
763 prerendering = false;
@@ -761,7 +825,9 @@ describe('ReactDOMFizzStaticBrowser', () => {
825 });
826 });
827
764 - controller.abort();
828 + await serverAct(() => {
829 + controller.abort();
830 + });
831
832 const prerendered = await pendingResult;
833 const postponedState = JSON.stringify(prerendered.postponed);
@@ -792,7 +858,9 @@ describe('ReactDOMFizzStaticBrowser', () => {
858 );
859 });
860
795 - controller2.abort();
861 + await serverAct(() => {
862 + controller2.abort();
863 + });
864
865 const prerendered2 = await pendingResult;
866 const postponedState2 = JSON.stringify(prerendered2.postponed);
@@ -855,7 +923,9 @@ describe('ReactDOMFizzStaticBrowser', () => {
923 });
924 });
925
858 - controller.abort(new Error('boom'));
926 + await serverAct(() => {
927 + controller.abort(new Error('boom'));
928 + });
929
930 const prerendered = await pendingResult;
931
@@ -917,7 +987,9 @@ describe('ReactDOMFizzStaticBrowser', () => {
987 });
988 });
989
920 - controller.abort();
990 + await serverAct(() => {
991 + controller.abort();
992 + });
993
994 const prerendered = await pendingResult;
995
@@ -995,7 +1067,9 @@ describe('ReactDOMFizzStaticBrowser', () => {
1067 });
1068 });
1069
998 - controller.abort();
1070 + await serverAct(() => {
1071 + controller.abort();
1072 + });
1073
1074 const prerendered = await pendingResult;
1075 const postponedState = JSON.stringify(prerendered.postponed);
@@ -1021,7 +1095,9 @@ describe('ReactDOMFizzStaticBrowser', () => {
1095 );
1096 });
1097
1024 - controller2.abort();
1098 + await serverAct(() => {
1099 + controller2.abort();
1100 + });
1101
1102 const prerendered2 = await pendingResult;
1103 const postponedState2 = JSON.stringify(prerendered2.postponed);
packages/react-dom/src/__tests__/ReactDOMFizzStaticNode-test.js
+53
@@ -216,6 +216,7 @@ describe('ReactDOMFizzStaticNode', () => {
216 await jest.runAllTimers();
217
218 controller.abort();
219 + await jest.runAllTimers();
220
221 const result = await resultPromise;
222
@@ -244,6 +245,7 @@ describe('ReactDOMFizzStaticNode', () => {
245
246 const theReason = new Error('aborted for reasons');
247 controller.abort(theReason);
248 + await jest.runAllTimers();
249
250 let didThrow = false;
251 let prelude;
@@ -281,6 +283,7 @@ describe('ReactDOMFizzStaticNode', () => {
283 },
284 );
285
286 + await jest.runAllTimers();
287 const {prelude} = await streamPromise;
288 const content = await readContent(prelude);
289 expect(errors).toEqual(['This operation was aborted']);
@@ -309,6 +312,7 @@ describe('ReactDOMFizzStaticNode', () => {
312
313 // Technically we could still continue rendering the shell but currently the
314 // semantics mean that we also abort any pending CPU work.
315 + await jest.runAllTimers();
316
317 let didThrow = false;
318 let prelude;
@@ -358,6 +362,7 @@ describe('ReactDOMFizzStaticNode', () => {
362 await jest.runAllTimers();
363
364 controller.abort('foobar');
365 + await jest.runAllTimers();
366
367 await resultPromise;
368
@@ -399,9 +404,57 @@ describe('ReactDOMFizzStaticNode', () => {
404 await jest.runAllTimers();
405
406 controller.abort(new Error('uh oh'));
407 + await jest.runAllTimers();
408
409 await resultPromise;
410
411 expect(errors).toEqual(['uh oh', 'uh oh']);
412 });
413 +
414 + it('currently uses the abort reason when an abort listener synchronously rejects pending work', async () => {
415 + let reject;
416 + const rejectedPromise = new Promise((resolve, rejectPromise) => {
417 + reject = rejectPromise;
418 + });
419 + const haltedPromise = new Promise(() => {});
420 + function RejectedWait() {
421 + React.use(rejectedPromise);
422 + return null;
423 + }
424 + function HaltedWait() {
425 + React.use(haltedPromise);
426 + return null;
427 + }
428 +
429 + const errors = [];
430 + const controller = new AbortController();
431 + const resultPromise = ReactDOMFizzStatic.prerenderToNodeStream(
432 + <>
433 + <Suspense fallback="Loading rejected">
434 + <RejectedWait />
435 + </Suspense>
436 + <Suspense fallback="Loading halted">
437 + <HaltedWait />
438 + </Suspense>
439 + </>,
440 + {
441 + signal: controller.signal,
442 + onError(error) {
443 + errors.push(error.message);
444 + },
445 + },
446 + );
447 +
448 + await jest.runAllTimers();
449 +
450 + controller.signal.addEventListener('abort', () => {
451 + reject(new Error('rejected during abort'));
452 + });
453 + controller.abort(new Error('abort reason'));
454 +
455 + await jest.runAllTimers();
456 + await resultPromise;
457 +
458 + expect(errors).toEqual(['abort reason', 'abort reason']);
459 + });
460 });
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js
+1 -1
@@ -2051,7 +2051,7 @@ describe('ReactFlightDOMNode', () => {
2051 );
2052
2053 expect(result).toContain(
2054 - 'Switched to client rendering because the server rendering aborted due to:\n\n' +
2054 + 'Switched to client rendering because the server rendering aborted due to:\\n\\n' +
2055 'ssr-abort',
2056 );
2057 });
packages/react-server/src/ReactFizzServer.js
+102 -41
@@ -356,6 +356,9 @@ type Segment = {
356 textEmbedded: boolean,
357 };
358
359 +// The ordering of these statuses matters. OPENING and OPEN are the only
360 +// statuses in which newly scheduled work may be performed. Any status greater
361 +// than OPEN represents a request that no longer admits work.
362 const OPENING = 10;
363 const OPEN = 11;
364 const CLOSING = 12;
@@ -4591,9 +4594,9 @@ function abortRemainingReplayNodes(
4594 }
4595 }
4596
4594 -function abortTask(task: Task, request: Request, error: mixed): void {
4595 - // This aborts the task and aborts the parent that it blocks, putting it into
4596 - // client rendered mode.
4597 +function abortTask(task: Task, request: Request): void {
4598 + // Mark pending tasks as aborted synchronously so any work that was already
4599 + // scheduled cannot begin after abort is called.
4600 if (task === request.currentTask) {
4601 // This is a currently rendering Task. The render itself will abort the task.
4602 return;
@@ -4604,17 +4607,13 @@ function abortTask(task: Task, request: Request, error: mixed): void {
4607 segment.status = ABORTED;
4608 }
4609
4607 - const errorInfo = getThrownInfo(task.componentStack);
4610 if (__DEV__ && enableAsyncDebugInfo) {
4609 - // If the task is not rendering, then this is an async abort. Conceptually it's as if
4610 - // the abort happened inside the async gap. The abort reason's stack frame won't have that
4611 - // on the stack so instead we use the owner stack and debug task of any halted async debug info.
4611 + // Capture async debug information at the point abort begins. The task may
4612 + // receive more data before finishAbort runs and no longer suspend at the
4613 + // call site we need to report.
4614 let node: any = task.node;
4615 if (node !== null && typeof node === 'object') {
4614 - // Push a fake component stack frame that represents the await.
4616 let debugInfo = node._debugInfo;
4616 - // First resolve lazy nodes to find debug info that has been transferred
4617 - // to the inner value.
4617 while (
4618 typeof node === 'object' &&
4619 node !== null &&
@@ -4645,6 +4644,29 @@ function abortTask(task: Task, request: Request, error: mixed): void {
4644 }
4645 }
4646
4647 + if (boundary !== null) {
4648 + boundary.fallbackAbortableTasks.forEach(fallbackTask =>
4649 + abortTask(fallbackTask, request),
4650 + );
4651 + }
4652 +}
4653 +
4654 +function finishAbortedTask(task: Task, request: Request, error: mixed): void {
4655 + // Report and complete a task that was synchronously claimed by abortTask.
4656 + // A currently rendering task remains responsible for unwinding itself.
4657 + if (task === request.currentTask) {
4658 + return;
4659 + }
4660 + const boundary = task.blockedBoundary;
4661 + const segment = task.blockedSegment;
4662 + if (segment !== null) {
4663 + if (segment.status !== ABORTED) {
4664 + return;
4665 + }
4666 + }
4667 +
4668 + const errorInfo = getThrownInfo(task.componentStack);
4669 +
4670 if (boundary === null) {
4671 const replay: null | ReplaySet = task.replay;
4672 if (replay === null) {
@@ -4706,7 +4728,7 @@ function abortTask(task: Task, request: Request, error: mixed): void {
4728 // If this boundary was still pending then we haven't already cancelled its fallbacks.
4729 // We'll need to abort the fallbacks, which will also error that parent boundary.
4730 boundary.fallbackAbortableTasks.forEach(fallbackTask =>
4709 - abortTask(fallbackTask, request, error),
4731 + finishAbortedTask(fallbackTask, request, error),
4732 );
4733 boundary.fallbackAbortableTasks.clear();
4734 return finishedTask(request, boundary, task.row, segment);
@@ -4743,7 +4765,7 @@ function abortTask(task: Task, request: Request, error: mixed): void {
4765 // If this boundary was still pending then we haven't already cancelled its fallbacks.
4766 // We'll need to abort the fallbacks, which will also error that parent boundary.
4767 boundary.fallbackAbortableTasks.forEach(fallbackTask =>
4746 - abortTask(fallbackTask, request, error),
4768 + finishAbortedTask(fallbackTask, request, error),
4769 );
4770 boundary.fallbackAbortableTasks.clear();
4771 }
@@ -4761,14 +4783,39 @@ function abortTask(task: Task, request: Request, error: mixed): void {
4783 }
4784 }
4785
4764 -function abortTaskDEV(task: Task, request: Request, error: mixed): void {
4786 +function finishAbortedTaskDEV(
4787 + task: Task,
4788 + request: Request,
4789 + error: mixed,
4790 +): void {
4791 if (__DEV__) {
4792 const prevTaskInDEV = currentTaskInDEV;
4793 const prevGetCurrentStackImpl = ReactSharedInternals.getCurrentStack;
4794 setCurrentTaskInDEV(task);
4795 ReactSharedInternals.getCurrentStack = getCurrentStackInDEV;
4796 try {
4771 - abortTask(task, request, error);
4797 + finishAbortedTask(task, request, error);
4798 + } finally {
4799 + setCurrentTaskInDEV(prevTaskInDEV);
4800 + ReactSharedInternals.getCurrentStack = prevGetCurrentStackImpl;
4801 + }
4802 + } else {
4803 + // These errors should never make it into a build so we don't need to encode them in codes.json
4804 + // eslint-disable-next-line react-internal/prod-error-codes
4805 + throw new Error(
4806 + 'finishAbortedTaskDEV should never be called in production mode. This is a bug in React.',
4807 + );
4808 + }
4809 +}
4810 +
4811 +function abortTaskDEV(task: Task, request: Request): void {
4812 + if (__DEV__) {
4813 + const prevTaskInDEV = currentTaskInDEV;
4814 + const prevGetCurrentStackImpl = ReactSharedInternals.getCurrentStack;
4815 + setCurrentTaskInDEV(task);
4816 + ReactSharedInternals.getCurrentStack = getCurrentStackInDEV;
4817 + try {
4818 + abortTask(task, request);
4819 } finally {
4820 setCurrentTaskInDEV(prevTaskInDEV);
4821 ReactSharedInternals.getCurrentStack = prevGetCurrentStackImpl;
@@ -5276,7 +5323,7 @@ function retryReplayTask(request: Request, task: ReplayTask): void {
5323 }
5324
5325 export function performWork(request: Request): void {
5279 - if (request.status === CLOSED || request.status === CLOSING) {
5326 + if (request.aborted || request.status > OPEN) {
5327 return;
5328 }
5329 const prevContext = getActiveContext();
@@ -6156,36 +6203,16 @@ export function stopFlowing(request: Request): void {
6203 request.destination = null;
6204 }
6205
6159 -// This is called to early terminate a request. It puts all pending boundaries in client rendered state.
6160 -export function abort(request: Request, reason: mixed): void {
6161 - if (
6162 - request.aborted ||
6163 - (request.status !== OPEN && request.status !== OPENING)
6164 - ) {
6165 - // Only requests that are not already complete or in the process of aborting
6166 - // can be aborted. in practice this makes abort callable at most once per render.
6167 - return;
6168 - }
6169 - request.aborted = true;
6170 -
6206 +function finishAbort(request: Request, abortableTasks: Set<Task>): void {
6207 try {
6172 - const abortableTasks = request.abortableTasks;
6208 if (abortableTasks.size > 0) {
6174 - const error =
6175 - reason === undefined
6176 - ? new Error('The render was aborted by the server without a reason.')
6177 - : typeof reason === 'object' &&
6178 - reason !== null &&
6179 - typeof reason.then === 'function'
6180 - ? new Error('The render was aborted by the server with a promise.')
6181 - : reason;
6182 - // This error isn't necessarily fatal in this case but we need to stash it
6183 - // so we can use it to abort any pending work
6184 - request.fatalError = error;
6209 + const error = request.fatalError;
6210 if (__DEV__) {
6186 - abortableTasks.forEach(task => abortTaskDEV(task, request, error));
6211 + abortableTasks.forEach(task =>
6212 + finishAbortedTaskDEV(task, request, error),
6213 + );
6214 } else {
6188 - abortableTasks.forEach(task => abortTask(task, request, error));
6215 + abortableTasks.forEach(task => finishAbortedTask(task, request, error));
6216 }
6217 abortableTasks.clear();
6218 }
@@ -6199,6 +6226,40 @@ export function abort(request: Request, reason: mixed): void {
6226 }
6227 }
6228
6229 +// This is called to early terminate a request. It puts all pending boundaries in client rendered state.
6230 +export function abort(request: Request, reason: mixed): void {
6231 + if (
6232 + request.aborted ||
6233 + (request.status !== OPEN && request.status !== OPENING)
6234 + ) {
6235 + // Only requests that are not already complete or in the process of aborting
6236 + // can be aborted. in practice this makes abort callable at most once per render.
6237 + return;
6238 + }
6239 + request.aborted = true;
6240 + const error =
6241 + reason === undefined
6242 + ? new Error('The render was aborted by the server without a reason.')
6243 + : typeof reason === 'object' &&
6244 + reason !== null &&
6245 + typeof reason.then === 'function'
6246 + ? new Error('The render was aborted by the server with a promise.')
6247 + : reason;
6248 + // This error isn't necessarily fatal in this case but we need to stash it
6249 + // so we can use it to abort any pending work.
6250 + request.fatalError = error;
6251 + const abortableTasks = request.abortableTasks;
6252 + if (__DEV__) {
6253 + abortableTasks.forEach(task => abortTaskDEV(task, request));
6254 + } else {
6255 + abortableTasks.forEach(task => abortTask(task, request));
6256 + }
6257 + // Even though this looks async some renderers schedule work sync
6258 + // So it is important that the finish step does not assume the stack
6259 + // has unwinded yet
6260 + scheduleWork(() => finishAbort(request, abortableTasks));
6261 +}
6262 +
6263 export function flushResources(request: Request): void {
6264 enqueueFlush(request);
6265 }