@samitouri / QOS-React-2 / commits / 63310df2b2

[Fizz] Add Component Stacks to `onError` and `onPostpone` when in dev mode or during prerenders in prod mode (#27761)

Historically React would produce component stacks for dev builds only. There is a cost to tracking component stacks and given the prod builds try to optimize runtime performance these stacks were left out. More recently React added production component stacks to Fiber in because it can be immensely helpful in tracking down hard to debug production issues. Fizz was not updated to have a similar behavior. With the advent of prerendering however stacks for production in Fizz are more relevant because prerendering is not really a dev-time task. If you want the ability to reason about errors or postpones that happen during a prerender having component stacks to interrogate is helpful and these component stacks need to be available in production otherwise you are really never going to see them. (it is possible that you could do dev-mode prerenders but we don't expect this to be a common dev mode workflow) To better support the prerender use case and to make error logging in Fizz more useful the following changes have been made 1. `onPostpone` now accepts a second `postponeInfo` argument which will contain a componentStack. Postpones always originate from a component render so the stack should be consistently available. The type however will indicate the stack is optional so we can remove them in the future if we decide the overhead is the wrong tradeoff in certain cases 2. `onError` now accepts a second `errorInfo` argument which may contain a componentStack. If an error originated from a component a stack will be included in the following cases. This change entails tracking the component hierarchy in prod builds now. While this isn't cost free it is implemented in a relatively lean manner. Deferring the most expensive work (reifying the stack) until we are actually in an error pathway. In the course of implementing this change a number of simplifications were made to the code which should make the stack tracking more resilient. We no longer use a module global to curry the stack up to some handler. This was delicate because you needed to always reset it properly. We now curry the stack on the task itself. Another change made was to track the component stack on SuspenseBoundary instances so that we can provide the stack when aborting suspense boundaries to help you determine which ones were affected by an abort.

Josh Story committed Dec 15, 2023 at 18:06 UTC 63310df2b243b6c3b2f01e8b121e7d115e839cfb
9 files changed +348 -294
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+45 -8
@@ -734,7 +734,7 @@ describe('ReactDOMFizzServer', () => {
734
735 const theError = new Error('Test');
736 const loggedErrors = [];
737 - function onError(x) {
737 + function onError(x, errorInfo) {
738 loggedErrors.push(x);
739 return 'Hash of (' + x.message + ')';
740 }
@@ -837,7 +837,7 @@ describe('ReactDOMFizzServer', () => {
837
838 const theError = new Error('Test');
839 const loggedErrors = [];
840 - function onError(x) {
840 + function onError(x, errorInfo) {
841 loggedErrors.push(x);
842 return 'hash of (' + x.message + ')';
843 }
@@ -898,7 +898,7 @@ describe('ReactDOMFizzServer', () => {
898 [
899 theError.message,
900 expectedDigest,
901 - componentStack(['Suspense', 'div', 'App']),
901 + componentStack(['Lazy', 'Suspense', 'div', 'App']),
902 ],
903 ],
904 [
@@ -936,7 +936,9 @@ describe('ReactDOMFizzServer', () => {
936 return (
937 <div>
938 <Suspense fallback={<span>loading...</span>}>
939 - <Erroring isClient={isClient} />
939 + <Indirection level={2}>
940 + <Erroring isClient={isClient} />
941 + </Indirection>
942 </Suspense>
943 </div>
944 );
@@ -979,7 +981,15 @@ describe('ReactDOMFizzServer', () => {
981 [
982 theError.message,
983 expectedDigest,
982 - componentStack(['Erroring', 'Suspense', 'div', 'App']),
984 + componentStack([
985 + 'Erroring',
986 + 'Indirection',
987 + 'Indirection',
988 + 'Indirection',
989 + 'Suspense',
990 + 'div',
991 + 'App',
992 + ]),
993 ],
994 ],
995 [
@@ -1330,6 +1340,11 @@ describe('ReactDOMFizzServer', () => {
1340 <AsyncText text="Hello" />
1341 </h1>
1342 </Suspense>
1343 + <main>
1344 + <Suspense fallback="loading...">
1345 + <AsyncText text="World" />
1346 + </Suspense>
1347 + </main>
1348 </div>
1349 );
1350 }
@@ -1359,7 +1374,11 @@ describe('ReactDOMFizzServer', () => {
1374 await waitForAll([]);
1375
1376 // We're still loading because we're waiting for the server to stream more content.
1362 - expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
1377 + expect(getVisibleChildren(container)).toEqual(
1378 + <div>
1379 + Loading...<main>loading...</main>
1380 + </div>,
1381 + );
1382
1383 // We abort the server response.
1384 await act(() => {
@@ -1374,26 +1393,44 @@ describe('ReactDOMFizzServer', () => {
1393 [
1394 'The server did not finish this Suspense boundary: The render was aborted by the server without a reason.',
1395 expectedDigest,
1396 + // We get the stack of the task when it was aborted which is why we see `h1`
1397 componentStack(['h1', 'Suspense', 'div', 'App']),
1398 ],
1399 + [
1400 + 'The server did not finish this Suspense boundary: The render was aborted by the server without a reason.',
1401 + expectedDigest,
1402 + componentStack(['Suspense', 'main', 'div', 'App']),
1403 + ],
1404 ],
1405 [
1406 [
1407 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
1408 expectedDigest,
1409 ],
1410 + [
1411 + 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
1412 + expectedDigest,
1413 + ],
1414 ],
1415 );
1387 - expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
1416 + expect(getVisibleChildren(container)).toEqual(
1417 + <div>
1418 + Loading...<main>loading...</main>
1419 + </div>,
1420 + );
1421
1422 // We now resolve it on the client.
1390 - await clientAct(() => resolveText('Hello'));
1423 + await clientAct(() => {
1424 + resolveText('Hello');
1425 + resolveText('World');
1426 + });
1427 assertLog([]);
1428
1429 // The client rendered HTML is now in place.
1430 expect(getVisibleChildren(container)).toEqual(
1431 <div>
1432 <h1>Hello</h1>
1433 + <main>World</main>
1434 </div>,
1435 );
1436 });
packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
+7 -3
@@ -7,7 +7,11 @@
7 * @flow
8 */
9
10 -import type {PostponedState} from 'react-server/src/ReactFizzServer';
10 +import type {
11 + PostponedState,
12 + ErrorInfo,
13 + PostponeInfo,
14 +} from 'react-server/src/ReactFizzServer';
15 import type {ReactNodeList, ReactFormState} from 'shared/ReactTypes';
16 import type {
17 BootstrapScriptDescriptor,
@@ -42,8 +46,8 @@ type Options = {
46 bootstrapModules?: Array<string | BootstrapScriptDescriptor>,
47 progressiveChunkSize?: number,
48 signal?: AbortSignal,
45 - onError?: (error: mixed) => ?string,
46 - onPostpone?: (reason: string) => void,
49 + onError?: (error: mixed, errorInfo: ErrorInfo) => ?string,
50 + onPostpone?: (reason: string, postponeInfo: PostponeInfo) => void,
51 unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
52 importMap?: ImportMap,
53 formState?: ReactFormState<any, any> | null,
packages/react-dom/src/server/ReactDOMFizzServerBun.js
+3 -2
@@ -13,6 +13,7 @@ import type {
13 HeadersDescriptor,
14 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
15 import type {ImportMap} from '../shared/ReactDOMTypes';
16 +import type {ErrorInfo, PostponeInfo} from 'react-server/src/ReactFizzServer';
17
18 import ReactVersion from 'shared/ReactVersion';
19
@@ -39,8 +40,8 @@ type Options = {
40 bootstrapModules?: Array<string | BootstrapScriptDescriptor>,
41 progressiveChunkSize?: number,
42 signal?: AbortSignal,
42 - onError?: (error: mixed) => ?string,
43 - onPostpone?: (reason: string) => void,
43 + onError?: (error: mixed, errorInfo: ErrorInfo) => ?string,
44 + onPostpone?: (reason: string, postponeInfo: PostponeInfo) => void,
45 unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
46 importMap?: ImportMap,
47 formState?: ReactFormState<any, any> | null,
packages/react-dom/src/server/ReactDOMFizzServerEdge.js
+7 -3
@@ -7,7 +7,11 @@
7 * @flow
8 */
9
10 -import type {PostponedState} from 'react-server/src/ReactFizzServer';
10 +import type {
11 + PostponedState,
12 + ErrorInfo,
13 + PostponeInfo,
14 +} from 'react-server/src/ReactFizzServer';
15 import type {ReactNodeList, ReactFormState} from 'shared/ReactTypes';
16 import type {
17 BootstrapScriptDescriptor,
@@ -42,8 +46,8 @@ type Options = {
46 bootstrapModules?: Array<string | BootstrapScriptDescriptor>,
47 progressiveChunkSize?: number,
48 signal?: AbortSignal,
45 - onError?: (error: mixed) => ?string,
46 - onPostpone?: (reason: string) => void,
49 + onError?: (error: mixed, errorInfo: ErrorInfo) => ?string,
50 + onPostpone?: (reason: string, postponeInfo: PostponeInfo) => void,
51 unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
52 importMap?: ImportMap,
53 formState?: ReactFormState<any, any> | null,
packages/react-dom/src/server/ReactDOMFizzServerNode.js
+10 -5
@@ -7,7 +7,12 @@
7 * @flow
8 */
9
10 -import type {Request, PostponedState} from 'react-server/src/ReactFizzServer';
10 +import type {
11 + Request,
12 + PostponedState,
13 + ErrorInfo,
14 + PostponeInfo,
15 +} from 'react-server/src/ReactFizzServer';
16 import type {ReactNodeList, ReactFormState} from 'shared/ReactTypes';
17 import type {Writable} from 'stream';
18 import type {
@@ -59,8 +64,8 @@ type Options = {
64 onShellReady?: () => void,
65 onShellError?: (error: mixed) => void,
66 onAllReady?: () => void,
62 - onError?: (error: mixed) => ?string,
63 - onPostpone?: (reason: string) => void,
67 + onError?: (error: mixed, errorInfo: ErrorInfo) => ?string,
68 + onPostpone?: (reason: string, postponeInfo: PostponeInfo) => void,
69 unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
70 importMap?: ImportMap,
71 formState?: ReactFormState<any, any> | null,
@@ -73,8 +78,8 @@ type ResumeOptions = {
78 onShellReady?: () => void,
79 onShellError?: (error: mixed) => void,
80 onAllReady?: () => void,
76 - onError?: (error: mixed) => ?string,
77 - onPostpone?: (reason: string) => void,
81 + onError?: (error: mixed, errorInfo: ErrorInfo) => ?string,
82 + onPostpone?: (reason: string, postponeInfo: PostponeInfo) => void,
83 };
84
85 type PipeableStream = {
packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js
+7 -3
@@ -12,7 +12,11 @@ import type {
12 BootstrapScriptDescriptor,
13 HeadersDescriptor,
14 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
15 -import type {PostponedState} from 'react-server/src/ReactFizzServer';
15 +import type {
16 + PostponedState,
17 + ErrorInfo,
18 + PostponeInfo,
19 +} from 'react-server/src/ReactFizzServer';
20 import type {ImportMap} from '../shared/ReactDOMTypes';
21
22 import ReactVersion from 'shared/ReactVersion';
@@ -40,8 +44,8 @@ type Options = {
44 bootstrapModules?: Array<string | BootstrapScriptDescriptor>,
45 progressiveChunkSize?: number,
46 signal?: AbortSignal,
43 - onError?: (error: mixed) => ?string,
44 - onPostpone?: (reason: string) => void,
47 + onError?: (error: mixed, errorInfo: ErrorInfo) => ?string,
48 + onPostpone?: (reason: string, postponeInfo: PostponeInfo) => void,
49 unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
50 importMap?: ImportMap,
51 onHeaders?: (headers: Headers) => void,
packages/react-dom/src/server/ReactDOMFizzStaticEdge.js
+7 -3
@@ -12,7 +12,11 @@ import type {
12 BootstrapScriptDescriptor,
13 HeadersDescriptor,
14 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
15 -import type {PostponedState} from 'react-server/src/ReactFizzServer';
15 +import type {
16 + PostponedState,
17 + ErrorInfo,
18 + PostponeInfo,
19 +} from 'react-server/src/ReactFizzServer';
20 import type {ImportMap} from '../shared/ReactDOMTypes';
21
22 import ReactVersion from 'shared/ReactVersion';
@@ -40,8 +44,8 @@ type Options = {
44 bootstrapModules?: Array<string | BootstrapScriptDescriptor>,
45 progressiveChunkSize?: number,
46 signal?: AbortSignal,
43 - onError?: (error: mixed) => ?string,
44 - onPostpone?: (reason: string) => void,
47 + onError?: (error: mixed, errorInfo: ErrorInfo) => ?string,
48 + onPostpone?: (reason: string, postponeInfo: PostponeInfo) => void,
49 unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
50 importMap?: ImportMap,
51 onHeaders?: (headers: Headers) => void,
packages/react-dom/src/server/ReactDOMFizzStaticNode.js
+7 -3
@@ -12,7 +12,11 @@ import type {
12 BootstrapScriptDescriptor,
13 HeadersDescriptor,
14 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
15 -import type {PostponedState} from 'react-server/src/ReactFizzServer';
15 +import type {
16 + PostponedState,
17 + ErrorInfo,
18 + PostponeInfo,
19 +} from 'react-server/src/ReactFizzServer';
20 import type {ImportMap} from '../shared/ReactDOMTypes';
21
22 import {Writable, Readable} from 'stream';
@@ -41,8 +45,8 @@ type Options = {
45 bootstrapModules?: Array<string | BootstrapScriptDescriptor>,
46 progressiveChunkSize?: number,
47 signal?: AbortSignal,
44 - onError?: (error: mixed) => ?string,
45 - onPostpone?: (reason: string) => void,
48 + onError?: (error: mixed, errorInfo: ErrorInfo) => ?string,
49 + onPostpone?: (reason: string, postponeInfo: PostponeInfo) => void,
50 unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
51 importMap?: ImportMap,
52 onHeaders?: (headers: HeadersDescriptor) => void,
packages/react-server/src/ReactFizzServer.js
+255 -264
@@ -229,7 +229,7 @@ type RenderTask = {
229 legacyContext: LegacyContext, // the current legacy context that this task is executing in
230 context: ContextSnapshot, // the current new context that this task is executing in
231 treeContext: TreeContext, // the current tree context that this task is executing in
232 - componentStack: null | ComponentStackNode, // DEV-only component stack
232 + componentStack: null | ComponentStackNode, // stack frame description of the currently rendering component
233 thenableState: null | ThenableState,
234 };
235
@@ -254,7 +254,7 @@ type ReplayTask = {
254 legacyContext: LegacyContext, // the current legacy context that this task is executing in
255 context: ContextSnapshot, // the current new context that this task is executing in
256 treeContext: TreeContext, // the current tree context that this task is executing in
257 - componentStack: null | ComponentStackNode, // DEV-only component stack
257 + componentStack: null | ComponentStackNode, // stack frame description of the currently rendering component
258 thenableState: null | ThenableState,
259 };
260
@@ -312,7 +312,7 @@ export opaque type Request = {
312 // onError is called when an error happens anywhere in the tree. It might recover.
313 // The return string is used in production primarily to avoid leaking internals, secondarily to save bytes.
314 // Returning null/undefined will cause a defualt error message in production
315 - onError: (error: mixed) => ?string,
315 + onError: (error: mixed, errorInfo: ThrownInfo) => ?string,
316 // onAllReady is called when all pending task is done but it may not have flushed yet.
317 // This is a good time to start writing if you want only HTML and no intermediate steps.
318 onAllReady: () => void,
@@ -326,7 +326,7 @@ export opaque type Request = {
326 onFatalError: (error: mixed) => void,
327 // onPostpone is called when postpone() is called anywhere in the tree, which will defer
328 // rendering - e.g. to the client. This is considered intentional and not an error.
329 - onPostpone: (reason: string) => void,
329 + onPostpone: (reason: string, postponeInfo: ThrownInfo) => void,
330 // Form state that was the result of an MPA submission, if it was provided.
331 formState: null | ReactFormState<any, any>,
332 };
@@ -361,12 +361,12 @@ export function createRequest(
361 renderState: RenderState,
362 rootFormatContext: FormatContext,
363 progressiveChunkSize: void | number,
364 - onError: void | ((error: mixed) => ?string),
364 + onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string),
365 onAllReady: void | (() => void),
366 onShellReady: void | (() => void),
367 onShellError: void | ((error: mixed) => void),
368 onFatalError: void | ((error: mixed) => void),
369 - onPostpone: void | ((reason: string) => void),
369 + onPostpone: void | ((reason: string, postponeInfo: PostponeInfo) => void),
370 formState: void | null | ReactFormState<any, any>,
371 ): Request {
372 prepareHostDispatcher();
@@ -427,6 +427,7 @@ export function createRequest(
427 emptyContextObject,
428 rootContextSnapshot,
429 emptyTreeContext,
430 + null,
431 );
432 pingedTasks.push(rootTask);
433 return request;
@@ -438,12 +439,12 @@ export function createPrerenderRequest(
439 renderState: RenderState,
440 rootFormatContext: FormatContext,
441 progressiveChunkSize: void | number,
441 - onError: void | ((error: mixed) => ?string),
442 + onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string),
443 onAllReady: void | (() => void),
444 onShellReady: void | (() => void),
445 onShellError: void | ((error: mixed) => void),
446 onFatalError: void | ((error: mixed) => void),
446 - onPostpone: void | ((reason: string) => void),
447 + onPostpone: void | ((reason: string, postponeInfo: PostponeInfo) => void),
448 ): Request {
449 const request = createRequest(
450 children,
@@ -472,12 +473,12 @@ export function resumeRequest(
473 children: ReactNodeList,
474 postponedState: PostponedState,
475 renderState: RenderState,
475 - onError: void | ((error: mixed) => ?string),
476 + onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string),
477 onAllReady: void | (() => void),
478 onShellReady: void | (() => void),
479 onShellError: void | ((error: mixed) => void),
480 onFatalError: void | ((error: mixed) => void),
480 - onPostpone: void | ((reason: string) => void),
481 + onPostpone: void | ((reason: string, postponeInfo: PostponeInfo) => void),
482 ): Request {
483 prepareHostDispatcher();
484 const pingedTasks: Array<Task> = [];
@@ -537,6 +538,7 @@ export function resumeRequest(
538 emptyContextObject,
539 rootContextSnapshot,
540 emptyTreeContext,
541 + null,
542 );
543 pingedTasks.push(rootTask);
544 return request;
@@ -560,6 +562,7 @@ export function resumeRequest(
562 emptyContextObject,
563 rootContextSnapshot,
564 emptyTreeContext,
565 + null,
566 );
567 pingedTasks.push(rootTask);
568 return request;
@@ -617,6 +620,7 @@ function createRenderTask(
620 legacyContext: LegacyContext,
621 context: ContextSnapshot,
622 treeContext: TreeContext,
623 + componentStack: null | ComponentStackNode,
624 ): RenderTask {
625 request.allPendingTasks++;
626 if (blockedBoundary === null) {
@@ -624,7 +628,7 @@ function createRenderTask(
628 } else {
629 blockedBoundary.pendingTasks++;
630 }
627 - const task: RenderTask = ({
631 + const task: RenderTask = {
632 replay: null,
633 node,
634 childIndex,
@@ -637,11 +641,9 @@ function createRenderTask(
641 legacyContext,
642 context,
643 treeContext,
644 + componentStack,
645 thenableState,
641 - }: any);
642 - if (__DEV__) {
643 - task.componentStack = null;
644 - }
646 + };
647 abortSet.add(task);
648 return task;
649 }
@@ -659,6 +661,7 @@ function createReplayTask(
661 legacyContext: LegacyContext,
662 context: ContextSnapshot,
663 treeContext: TreeContext,
664 + componentStack: null | ComponentStackNode,
665 ): ReplayTask {
666 request.allPendingTasks++;
667 if (blockedBoundary === null) {
@@ -667,7 +670,7 @@ function createReplayTask(
670 blockedBoundary.pendingTasks++;
671 }
672 replay.pendingTasks++;
670 - const task: ReplayTask = ({
673 + const task: ReplayTask = {
674 replay,
675 node,
676 childIndex,
@@ -680,11 +683,9 @@ function createReplayTask(
683 legacyContext,
684 context,
685 treeContext,
686 + componentStack,
687 thenableState,
684 - }: any);
685 - if (__DEV__) {
686 - task.componentStack = null;
687 - }
688 + };
689 abortSet.add(task);
690 return task;
691 }
@@ -723,53 +724,70 @@ function getCurrentStackInDEV(): string {
724 return '';
725 }
726
726 -function pushBuiltInComponentStackInDEV(task: Task, type: string): void {
727 - if (__DEV__) {
728 - task.componentStack = {
729 - tag: 0,
730 - parent: task.componentStack,
731 - type,
732 - };
733 - }
727 +function getStackFromNode(stackNode: ComponentStackNode): string {
728 + return getStackByComponentStackNode(stackNode);
729 }
735 -function pushFunctionComponentStackInDEV(task: Task, type: Function): void {
736 - if (__DEV__) {
737 - task.componentStack = {
738 - tag: 1,
739 - parent: task.componentStack,
740 - type,
741 - };
742 - }
730 +
731 +function createBuiltInComponentStack(
732 + task: Task,
733 + type: string,
734 +): ComponentStackNode {
735 + return {
736 + tag: 0,
737 + parent: task.componentStack,
738 + type,
739 + };
740 }
744 -function pushClassComponentStackInDEV(task: Task, type: Function): void {
745 - if (__DEV__) {
746 - task.componentStack = {
747 - tag: 2,
748 - parent: task.componentStack,
749 - type,
750 - };
751 - }
741 +function createFunctionComponentStack(
742 + task: Task,
743 + type: Function,
744 +): ComponentStackNode {
745 + return {
746 + tag: 1,
747 + parent: task.componentStack,
748 + type,
749 + };
750 }
753 -function popComponentStackInDEV(task: Task): void {
754 - if (__DEV__) {
755 - if (task.componentStack === null) {
756 - console.error(
757 - 'Unexpectedly popped too many stack frames. This is a bug in React.',
758 - );
759 - } else {
760 - task.componentStack = task.componentStack.parent;
761 - }
762 - }
751 +function createClassComponentStack(
752 + task: Task,
753 + type: Function,
754 +): ComponentStackNode {
755 + return {
756 + tag: 2,
757 + parent: task.componentStack,
758 + type,
759 + };
760 }
761
765 -// stash the component stack of an unwinding error until it is processed
766 -let lastBoundaryErrorComponentStackDev: ?string = null;
762 +type ThrownInfo = {
763 + componentStack?: string,
764 +};
765 +export type ErrorInfo = ThrownInfo;
766 +export type PostponeInfo = ThrownInfo;
767 +
768 +// While we track component stacks in prod all the time we only produce a reified stack in dev and
769 +// during prerender in Prod. The reason for this is that the stack is useful for prerender where the timeliness
770 +// of the request is less critical than the observability of the execution. For renders and resumes however we
771 +// prioritize speed of the request.
772 +function getThrownInfo(node: null | ComponentStackNode): ThrownInfo {
773 + if (node) {
774 + return {
775 + componentStack: getStackFromNode(node),
776 + };
777 + } else {
778 + return {};
779 + }
780 +}
781
768 -function captureBoundaryErrorDetailsDev(
782 +function encodeErrorForBoundary(
783 boundary: SuspenseBoundary,
784 + digest: ?string,
785 error: mixed,
786 + thrownInfo: ThrownInfo,
787 ) {
788 + boundary.errorDigest = digest;
789 if (__DEV__) {
790 + // In dev we additionally encode the error message and component stack on the boundary
791 let errorMessage;
792 if (typeof error === 'string') {
793 errorMessage = error;
@@ -780,30 +798,39 @@ function captureBoundaryErrorDetailsDev(
798 errorMessage = String(error);
799 }
800
783 - const errorComponentStack =
784 - lastBoundaryErrorComponentStackDev || getCurrentStackInDEV();
785 - lastBoundaryErrorComponentStackDev = null;
786 -
801 boundary.errorMessage = errorMessage;
788 - boundary.errorComponentStack = errorComponentStack;
802 + boundary.errorComponentStack = thrownInfo.componentStack;
803 }
804 }
805
792 -function logPostpone(request: Request, reason: string): void {
806 +function logPostpone(
807 + request: Request,
808 + reason: string,
809 + postponeInfo: ThrownInfo,
810 +): void {
811 // If this callback errors, we intentionally let that error bubble up to become a fatal error
812 // so that someone fixes the error reporting instead of hiding it.
795 - request.onPostpone(reason);
813 + request.onPostpone(reason, postponeInfo);
814 }
815
798 -function logRecoverableError(request: Request, error: any): ?string {
816 +function logRecoverableError(
817 + request: Request,
818 + error: any,
819 + errorInfo: ThrownInfo,
820 +): ?string {
821 // If this callback errors, we intentionally let that error bubble up to become a fatal error
822 // so that someone fixes the error reporting instead of hiding it.
801 - const errorDigest = request.onError(error);
823 + const errorDigest = request.onError(error, errorInfo);
824 if (errorDigest != null && typeof errorDigest !== 'string') {
803 - // eslint-disable-next-line react-internal/prod-error-codes
804 - throw new Error(
805 - `onError returned something with a type other than "string". onError should return a string and may return null or undefined but must not return anything else. It received something of type "${typeof errorDigest}" instead`,
806 - );
825 + // We used to throw here but since this gets called from a variety of unprotected places it
826 + // seems better to just warn and discard the returned value.
827 + if (__DEV__) {
828 + console.error(
829 + 'onError returned something with a type other than "string". onError should return a string and may return null or undefined but must not return anything else. It received something of type "%s" instead',
830 + typeof errorDigest,
831 + );
832 + }
833 + return;
834 }
835 return errorDigest;
836 }
@@ -848,7 +875,11 @@ function renderSuspenseBoundary(
875 // $FlowFixMe: Refined.
876 const task: RenderTask = someTask;
877
851 - pushBuiltInComponentStackInDEV(task, 'Suspense');
878 + const previousComponentStack = task.componentStack;
879 + // If we end up creating the fallback task we need it to have the correct stack which is
880 + // the stack for the boundary itself. We stash it here so we can use it if needed later
881 + const suspenseComponentStack = (task.componentStack =
882 + createBuiltInComponentStack(task, 'Suspense'));
883
884 const prevKeyPath = task.keyPath;
885 const parentBoundary = task.blockedBoundary;
@@ -912,6 +943,7 @@ function renderSuspenseBoundary(
943 );
944 }
945 task.keyPath = keyPath;
946 +
947 try {
948 // We use the safe form because we don't handle suspending here. Only error handling.
949 renderNode(request, task, content, -1);
@@ -924,16 +956,19 @@ function renderSuspenseBoundary(
956 contentRootSegment.status = COMPLETED;
957 queueCompletedSegment(newBoundary, contentRootSegment);
958 if (newBoundary.pendingTasks === 0 && newBoundary.status === PENDING) {
927 - newBoundary.status = COMPLETED;
959 // This must have been the last segment we were waiting on. This boundary is now complete.
960 // Therefore we won't need the fallback. We early return so that we don't have to create
961 // the fallback.
931 - popComponentStackInDEV(task);
962 + newBoundary.status = COMPLETED;
963 +
964 + // We are returning early so we need to restore the
965 + task.componentStack = previousComponentStack;
966 return;
967 }
934 - } catch (error) {
968 + } catch (error: mixed) {
969 contentRootSegment.status = ERRORED;
970 newBoundary.status = CLIENT_RENDERED;
971 + const thrownInfo = getThrownInfo(task.componentStack);
972 let errorDigest;
973 if (
974 enablePostpone &&
@@ -942,16 +977,13 @@ function renderSuspenseBoundary(
977 error.$$typeof === REACT_POSTPONE_TYPE
978 ) {
979 const postponeInstance: Postpone = (error: any);
945 - logPostpone(request, postponeInstance.message);
980 + logPostpone(request, postponeInstance.message, thrownInfo);
981 // TODO: Figure out a better signal than a magic digest value.
982 errorDigest = 'POSTPONE';
983 } else {
949 - errorDigest = logRecoverableError(request, error);
950 - }
951 - newBoundary.errorDigest = errorDigest;
952 - if (__DEV__) {
953 - captureBoundaryErrorDetailsDev(newBoundary, error);
984 + errorDigest = logRecoverableError(request, error, thrownInfo);
985 }
986 + encodeErrorForBoundary(newBoundary, errorDigest, error, thrownInfo);
987
988 // We don't need to decrement any task numbers because we didn't spawn any new task.
989 // We don't need to schedule any task because we know the parent has written yet.
@@ -966,6 +998,7 @@ function renderSuspenseBoundary(
998 task.blockedBoundary = parentBoundary;
999 task.blockedSegment = parentSegment;
1000 task.keyPath = prevKeyPath;
1001 + task.componentStack = previousComponentStack;
1002 }
1003
1004 const fallbackKeyPath = [keyPath[0], 'Suspense Fallback', keyPath[2]];
@@ -1005,15 +1038,13 @@ function renderSuspenseBoundary(
1038 task.legacyContext,
1039 task.context,
1040 task.treeContext,
1041 + // This stack should be the Suspense boundary stack because while the fallback is actually a child segment
1042 + // of the parent boundary from a component standpoint the fallback is a child of the Suspense boundary itself
1043 + suspenseComponentStack,
1044 );
1009 - if (__DEV__) {
1010 - suspendedFallbackTask.componentStack = task.componentStack;
1011 - }
1045 // TODO: This should be queued at a separate lower priority queue so that we only work
1046 // on preparing fallbacks if we don't have any more main content to task on.
1047 request.pingedTasks.push(suspendedFallbackTask);
1015 -
1016 - popComponentStackInDEV(task);
1048 }
1049
1050 function replaySuspenseBoundary(
@@ -1027,7 +1058,11 @@ function replaySuspenseBoundary(
1058 fallbackNodes: Array<ReplayNode>,
1059 fallbackSlots: ResumeSlots,
1060 ): void {
1030 - pushBuiltInComponentStackInDEV(task, 'Suspense');
1061 + const previousComponentStack = task.componentStack;
1062 + // If we end up creating the fallback task we need it to have the correct stack which is
1063 + // the stack for the boundary itself. We stash it here so we can use it if needed later
1064 + const suspenseComponentStack = (task.componentStack =
1065 + createBuiltInComponentStack(task, 'Suspense'));
1066
1067 const prevKeyPath = task.keyPath;
1068 const previousReplaySet: ReplaySet = task.replay;
@@ -1054,9 +1089,11 @@ function replaySuspenseBoundary(
1089 resumedBoundary.resources,
1090 );
1091 }
1092 +
1093 try {
1094 // We use the safe form because we don't handle suspending here. Only error handling.
1095 renderNode(request, task, content, -1);
1096 +
1097 if (task.replay.pendingTasks === 1 && task.replay.nodes.length > 0) {
1098 throw new Error(
1099 "Couldn't find all resumable slots by key/index during replaying. " +
@@ -1068,16 +1105,19 @@ function replaySuspenseBoundary(
1105 resumedBoundary.pendingTasks === 0 &&
1106 resumedBoundary.status === PENDING
1107 ) {
1071 - resumedBoundary.status = COMPLETED;
1072 - request.completedBoundaries.push(resumedBoundary);
1108 // This must have been the last segment we were waiting on. This boundary is now complete.
1109 // Therefore we won't need the fallback. We early return so that we don't have to create
1110 // the fallback.
1076 - popComponentStackInDEV(task);
1111 + resumedBoundary.status = COMPLETED;
1112 + request.completedBoundaries.push(resumedBoundary);
1113 + // We restore the parent componentStack. Semantically this is the same as
1114 + // popComponentStack(task) but we do this instead because it should be slightly
1115 + // faster
1116 return;
1117 }
1079 - } catch (error) {
1118 + } catch (error: mixed) {
1119 resumedBoundary.status = CLIENT_RENDERED;
1120 + const thrownInfo = getThrownInfo(task.componentStack);
1121 let errorDigest;
1122 if (
1123 enablePostpone &&
@@ -1086,16 +1126,13 @@ function replaySuspenseBoundary(
1126 error.$$typeof === REACT_POSTPONE_TYPE
1127 ) {
1128 const postponeInstance: Postpone = (error: any);
1089 - logPostpone(request, postponeInstance.message);
1129 + logPostpone(request, postponeInstance.message, thrownInfo);
1130 // TODO: Figure out a better signal than a magic digest value.
1131 errorDigest = 'POSTPONE';
1132 } else {
1093 - errorDigest = logRecoverableError(request, error);
1094 - }
1095 - resumedBoundary.errorDigest = errorDigest;
1096 - if (__DEV__) {
1097 - captureBoundaryErrorDetailsDev(resumedBoundary, error);
1133 + errorDigest = logRecoverableError(request, error, thrownInfo);
1134 }
1135 + encodeErrorForBoundary(resumedBoundary, errorDigest, error, thrownInfo);
1136
1137 task.replay.pendingTasks--;
1138
@@ -1115,6 +1152,7 @@ function replaySuspenseBoundary(
1152 task.blockedBoundary = parentBoundary;
1153 task.replay = previousReplaySet;
1154 task.keyPath = prevKeyPath;
1155 + task.componentStack = previousComponentStack;
1156 }
1157
1158 const fallbackKeyPath = [keyPath[0], 'Suspense Fallback', keyPath[2]];
@@ -1139,15 +1177,13 @@ function replaySuspenseBoundary(
1177 task.legacyContext,
1178 task.context,
1179 task.treeContext,
1180 + // This stack should be the Suspense boundary stack because while the fallback is actually a child segment
1181 + // of the parent boundary from a component standpoint the fallback is a child of the Suspense boundary itself
1182 + suspenseComponentStack,
1183 );
1143 - if (__DEV__) {
1144 - suspendedFallbackTask.componentStack = task.componentStack;
1145 - }
1184 // TODO: This should be queued at a separate lower priority queue so that we only work
1185 // on preparing fallbacks if we don't have any more main content to task on.
1186 request.pingedTasks.push(suspendedFallbackTask);
1149 -
1150 - popComponentStackInDEV(task);
1187 }
1188
1189 function renderBackupSuspenseBoundary(
@@ -1156,7 +1192,8 @@ function renderBackupSuspenseBoundary(
1192 keyPath: KeyNode,
1193 props: Object,
1194 ) {
1159 - pushBuiltInComponentStackInDEV(task, 'Suspense');
1195 + const previousComponentStack = task.componentStack;
1196 + task.componentStack = createBuiltInComponentStack(task, 'Suspense');
1197
1198 const content = props.children;
1199 const segment = task.blockedSegment;
@@ -1172,8 +1209,7 @@ function renderBackupSuspenseBoundary(
1209 pushEndCompletedSuspenseBoundary(segment.chunks);
1210 }
1211 task.keyPath = prevKeyPath;
1175 -
1176 - popComponentStackInDEV(task);
1212 + task.componentStack = previousComponentStack;
1213 }
1214
1215 function renderHostElement(
@@ -1183,7 +1219,8 @@ function renderHostElement(
1219 type: string,
1220 props: Object,
1221 ): void {
1186 - pushBuiltInComponentStackInDEV(task, type);
1222 + const previousComponentStack = task.componentStack;
1223 + task.componentStack = createBuiltInComponentStack(task, type);
1224 const segment = task.blockedSegment;
1225 if (segment === null) {
1226 // Replay
@@ -1235,7 +1272,7 @@ function renderHostElement(
1272 );
1273 segment.lastPushedText = false;
1274 }
1238 - popComponentStackInDEV(task);
1275 + task.componentStack = previousComponentStack;
1276 }
1277
1278 function shouldConstruct(Component: any) {
@@ -1316,14 +1353,15 @@ function renderClassComponent(
1353 Component: any,
1354 props: any,
1355 ): void {
1319 - pushClassComponentStackInDEV(task, Component);
1356 + const previousComponentStack = task.componentStack;
1357 + task.componentStack = createClassComponentStack(task, Component);
1358 const maskedContext = !disableLegacyContext
1359 ? getMaskedContext(Component, task.legacyContext)
1360 : undefined;
1361 const instance = constructClassInstance(Component, props, maskedContext);
1362 mountClassInstance(instance, Component, props, maskedContext);
1363 finishClassComponent(request, task, keyPath, instance, Component, props);
1326 - popComponentStackInDEV(task);
1364 + task.componentStack = previousComponentStack;
1365 }
1366
1367 const didWarnAboutBadClass: {[string]: boolean} = {};
@@ -1350,7 +1388,8 @@ function renderIndeterminateComponent(
1388 if (!disableLegacyContext) {
1389 legacyContext = getMaskedContext(Component, task.legacyContext);
1390 }
1353 - pushFunctionComponentStackInDEV(task, Component);
1391 + const previousComponentStack = task.componentStack;
1392 + task.componentStack = createFunctionComponentStack(task, Component);
1393
1394 if (__DEV__) {
1395 if (
@@ -1462,7 +1501,7 @@ function renderIndeterminateComponent(
1501 formStateMatchingIndex,
1502 );
1503 }
1465 - popComponentStackInDEV(task);
1504 + task.componentStack = previousComponentStack;
1505 }
1506
1507 function finishFunctionComponent(
@@ -1601,7 +1640,8 @@ function renderForwardRef(
1640 props: Object,
1641 ref: any,
1642 ): void {
1604 - pushFunctionComponentStackInDEV(task, type.render);
1643 + const previousComponentStack = task.componentStack;
1644 + task.componentStack = createFunctionComponentStack(task, type.render);
1645 const children = renderWithHooks(
1646 request,
1647 task,
@@ -1623,7 +1663,7 @@ function renderForwardRef(
1663 formStateCount,
1664 formStateMatchingIndex,
1665 );
1626 - popComponentStackInDEV(task);
1666 + task.componentStack = previousComponentStack;
1667 }
1668
1669 function renderMemo(
@@ -1740,7 +1780,8 @@ function renderLazyComponent(
1780 props: Object,
1781 ref: any,
1782 ): void {
1743 - pushBuiltInComponentStackInDEV(task, 'Lazy');
1783 + const previousComponentStack = task.componentStack;
1784 + task.componentStack = createBuiltInComponentStack(task, 'Lazy');
1785 const payload = lazyComponent._payload;
1786 const init = lazyComponent._init;
1787 const Component = init(payload);
@@ -1754,7 +1795,7 @@ function renderLazyComponent(
1795 resolvedProps,
1796 ref,
1797 );
1757 - popComponentStackInDEV(task);
1798 + task.componentStack = previousComponentStack;
1799 }
1800
1801 function renderOffscreen(
@@ -1833,13 +1874,14 @@ function renderElement(
1874 return;
1875 }
1876 case REACT_SUSPENSE_LIST_TYPE: {
1836 - pushBuiltInComponentStackInDEV(task, 'SuspenseList');
1877 + const preiousComponentStack = task.componentStack;
1878 + task.componentStack = createBuiltInComponentStack(task, 'SuspenseList');
1879 // TODO: SuspenseList should control the boundaries.
1880 const prevKeyPath = task.keyPath;
1881 task.keyPath = keyPath;
1882 renderNodeDestructive(request, task, null, props.children, -1);
1883 task.keyPath = prevKeyPath;
1842 - popComponentStackInDEV(task);
1884 + task.componentStack = preiousComponentStack;
1885 return;
1886 }
1887 case REACT_SCOPE_TYPE: {
@@ -2046,7 +2088,15 @@ function replayElement(
2088 // in the original prerender. What's unable to complete is the child
2089 // replay nodes which might be Suspense boundaries which are able to
2090 // absorb the error and we can still continue with siblings.
2049 - erroredReplay(request, task.blockedBoundary, x, childNodes, childSlots);
2091 + const thrownInfo = getThrownInfo(task.componentStack);
2092 + erroredReplay(
2093 + request,
2094 + task.blockedBoundary,
2095 + x,
2096 + thrownInfo,
2097 + childNodes,
2098 + childSlots,
2099 + );
2100 }
2101 task.replay = replay;
2102 } else {
@@ -2118,6 +2168,8 @@ function validateIterable(iterable, iteratorFn: Function): void {
2168 }
2169 }
2170
2171 +// This function by it self renders a node and consumes the task by mutating it
2172 +// to update the current execution state.
2173 function renderNodeDestructive(
2174 request: Request,
2175 task: Task,
@@ -2126,51 +2178,6 @@ function renderNodeDestructive(
2178 prevThenableState: ThenableState | null,
2179 node: ReactNodeList,
2180 childIndex: number,
2129 -): void {
2130 - if (__DEV__) {
2131 - // In Dev we wrap renderNodeDestructiveImpl in a try / catch so we can capture
2132 - // a component stack at the right place in the tree. We don't do this in renderNode
2133 - // becuase it is not called at every layer of the tree and we may lose frames
2134 - try {
2135 - return renderNodeDestructiveImpl(
2136 - request,
2137 - task,
2138 - prevThenableState,
2139 - node,
2140 - childIndex,
2141 - );
2142 - } catch (x) {
2143 - if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
2144 - // This is a Wakable, noop
2145 - } else {
2146 - // This is an error, stash the component stack if it is null.
2147 - lastBoundaryErrorComponentStackDev =
2148 - lastBoundaryErrorComponentStackDev !== null
2149 - ? lastBoundaryErrorComponentStackDev
2150 - : getCurrentStackInDEV();
2151 - }
2152 - // rethrow so normal suspense logic can handle thrown value accordingly
2153 - throw x;
2154 - }
2155 - } else {
2156 - return renderNodeDestructiveImpl(
2157 - request,
2158 - task,
2159 - prevThenableState,
2160 - node,
2161 - childIndex,
2162 - );
2163 - }
2164 -}
2165 -
2166 -// This function by it self renders a node and consumes the task by mutating it
2167 -// to update the current execution state.
2168 -function renderNodeDestructiveImpl(
2169 - request: Request,
2170 - task: Task,
2171 - prevThenableState: ThenableState | null,
2172 - node: ReactNodeList,
2173 - childIndex: number,
2181 ): void {
2182 if (task.replay !== null && typeof task.replay.slots === 'number') {
2183 // TODO: Figure out a cheaper place than this hot path to do this check.
@@ -2232,30 +2239,18 @@ function renderNodeDestructiveImpl(
2239 'Render them conditionally so that they only appear on the client render.',
2240 );
2241 case REACT_LAZY_TYPE: {
2242 + const previousComponentStack = task.componentStack;
2243 + task.componentStack = createBuiltInComponentStack(task, 'Lazy');
2244 const lazyNode: LazyComponentType<any, any> = (node: any);
2245 const payload = lazyNode._payload;
2246 const init = lazyNode._init;
2238 - let resolvedNode;
2239 - if (__DEV__) {
2240 - try {
2241 - resolvedNode = init(payload);
2242 - } catch (x) {
2243 - if (
2244 - typeof x === 'object' &&
2245 - x !== null &&
2246 - typeof x.then === 'function'
2247 - ) {
2248 - // this Lazy initializer is suspending. push a temporary frame onto the stack so it can be
2249 - // popped off in spawnNewSuspendedTask. This aligns stack behavior between Lazy in element position
2250 - // vs Component position. We do not want the frame for Errors so we exclusively do this in
2251 - // the wakeable branch
2252 - pushBuiltInComponentStackInDEV(task, 'Lazy');
2253 - }
2254 - throw x;
2255 - }
2256 - } else {
2257 - resolvedNode = init(payload);
2258 - }
2247 + const resolvedNode = init(payload);
2248 +
2249 + // We restore the stack before rendering the resolved node because once the Lazy
2250 + // has resolved any future errors
2251 + task.componentStack = previousComponentStack;
2252 +
2253 + // Now we render the resolved node
2254 renderNodeDestructive(request, task, null, resolvedNode, childIndex);
2255 return;
2256 }
@@ -2305,7 +2300,7 @@ function renderNodeDestructiveImpl(
2300 const maybeUsable: Object = node;
2301 if (typeof maybeUsable.then === 'function') {
2302 const thenable: Thenable<ReactNodeList> = (maybeUsable: any);
2308 - return renderNodeDestructiveImpl(
2303 + return renderNodeDestructive(
2304 request,
2305 task,
2306 null,
@@ -2319,7 +2314,7 @@ function renderNodeDestructiveImpl(
2314 maybeUsable.$$typeof === REACT_SERVER_CONTEXT_TYPE
2315 ) {
2316 const context: ReactContext<ReactNodeList> = (maybeUsable: any);
2322 - return renderNodeDestructiveImpl(
2317 + return renderNodeDestructive(
2318 request,
2319 task,
2320 null,
@@ -2429,7 +2424,15 @@ function replayFragment(
2424 // replay nodes which might be Suspense boundaries which are able to
2425 // absorb the error and we can still continue with siblings.
2426 // This is an error, stash the component stack if it is null.
2432 - erroredReplay(request, task.blockedBoundary, x, childNodes, childSlots);
2427 + const thrownInfo = getThrownInfo(task.componentStack);
2428 + erroredReplay(
2429 + request,
2430 + task.blockedBoundary,
2431 + x,
2432 + thrownInfo,
2433 + childNodes,
2434 + childSlots,
2435 + );
2436 }
2437 task.replay = replay;
2438 // We finished rendering this node, so now we can consume this
@@ -2664,8 +2667,9 @@ function injectPostponedHole(
2667 request: Request,
2668 task: RenderTask,
2669 reason: string,
2670 + thrownInfo: ThrownInfo,
2671 ): Segment {
2668 - logPostpone(request, reason);
2672 + logPostpone(request, reason, thrownInfo);
2673 // Something suspended, we'll need to create a new segment and resolve it later.
2674 const segment = task.blockedSegment;
2675 const insertionIndex = segment.chunks.length;
@@ -2704,15 +2708,11 @@ function spawnNewSuspendedReplayTask(
2708 task.legacyContext,
2709 task.context,
2710 task.treeContext,
2711 + // We pop one task off the stack because the node that suspended will be tried again,
2712 + // which will add it back onto the stack.
2713 + task.componentStack !== null ? task.componentStack.parent : null,
2714 );
2715
2709 - if (__DEV__) {
2710 - if (task.componentStack !== null) {
2711 - // We pop one task off the stack because the node that suspended will be tried again,
2712 - // which will add it back onto the stack.
2713 - newTask.componentStack = task.componentStack.parent;
2714 - }
2715 - }
2716 const ping = newTask.ping;
2717 x.then(ping, ping);
2718 }
@@ -2752,15 +2752,11 @@ function spawnNewSuspendedRenderTask(
2752 task.legacyContext,
2753 task.context,
2754 task.treeContext,
2755 + // We pop one task off the stack because the node that suspended will be tried again,
2756 + // which will add it back onto the stack.
2757 + task.componentStack !== null ? task.componentStack.parent : null,
2758 );
2759
2757 - if (__DEV__) {
2758 - if (task.componentStack !== null) {
2759 - // We pop one task off the stack because the node that suspended will be tried again,
2760 - // which will add it back onto the stack.
2761 - newTask.componentStack = task.componentStack.parent;
2762 - }
2763 - }
2760 const ping = newTask.ping;
2761 x.then(ping, ping);
2762 }
@@ -2780,10 +2776,7 @@ function renderNode(
2776 const previousContext = task.context;
2777 const previousKeyPath = task.keyPath;
2778 const previousTreeContext = task.treeContext;
2783 - let previousComponentStack = null;
2784 - if (__DEV__) {
2785 - previousComponentStack = task.componentStack;
2786 - }
2779 + const previousComponentStack = task.componentStack;
2780 let x;
2781 // Store how much we've pushed at this point so we can reset it in case something
2782 // suspended partially through writing something.
@@ -2825,11 +2818,9 @@ function renderNode(
2818 task.context = previousContext;
2819 task.keyPath = previousKeyPath;
2820 task.treeContext = previousTreeContext;
2821 + task.componentStack = previousComponentStack;
2822 // Restore all active ReactContexts to what they were before.
2823 switchContext(previousContext);
2830 - if (__DEV__) {
2831 - task.componentStack = previousComponentStack;
2832 - }
2824 return;
2825 }
2826 }
@@ -2879,29 +2870,30 @@ function renderNode(
2870 task.context = previousContext;
2871 task.keyPath = previousKeyPath;
2872 task.treeContext = previousTreeContext;
2873 + task.componentStack = previousComponentStack;
2874 // Restore all active ReactContexts to what they were before.
2875 switchContext(previousContext);
2884 - if (__DEV__) {
2885 - task.componentStack = previousComponentStack;
2886 - }
2876 return;
2877 }
2878 if (
2879 enablePostpone &&
2891 - request.trackedPostpones !== null &&
2880 x.$$typeof === REACT_POSTPONE_TYPE &&
2881 + request.trackedPostpones !== null &&
2882 task.blockedBoundary !== null // bubble if we're postponing in the shell
2883 ) {
2884 // If we're tracking postpones, we inject a hole here and continue rendering
2885 // sibling. Similar to suspending. If we're not tracking, we treat it more like
2886 // an error. Notably this doesn't spawn a new task since nothing will fill it
2887 // in during this prerender.
2899 - const postponeInstance: Postpone = (x: any);
2888 const trackedPostpones = request.trackedPostpones;
2889 +
2890 + const postponeInstance: Postpone = (x: any);
2891 + const thrownInfo = getThrownInfo(task.componentStack);
2892 const postponedSegment = injectPostponedHole(
2893 request,
2894 ((task: any): RenderTask), // We don't use ReplayTasks in prerenders.
2895 postponeInstance.message,
2896 + thrownInfo,
2897 );
2898 trackPostpone(request, trackedPostpones, task, postponedSegment);
2899
@@ -2912,17 +2904,15 @@ function renderNode(
2904 task.context = previousContext;
2905 task.keyPath = previousKeyPath;
2906 task.treeContext = previousTreeContext;
2907 + task.componentStack = previousComponentStack;
2908 // Restore all active ReactContexts to what they were before.
2909 switchContext(previousContext);
2917 - if (__DEV__) {
2918 - task.componentStack = previousComponentStack;
2919 - }
2920 - lastBoundaryErrorComponentStackDev = null;
2910 return;
2911 }
2912 }
2913 }
2914 }
2915 +
2916 // Restore the context. We assume that this will be restored by the inner
2917 // functions in case nothing throws so we don't use "finally" here.
2918 task.formatContext = previousFormatContext;
@@ -2930,13 +2920,13 @@ function renderNode(
2920 task.context = previousContext;
2921 task.keyPath = previousKeyPath;
2922 task.treeContext = previousTreeContext;
2923 + // We intentionally do not restore the component stack on the error pathway
2924 + // Whatever handles the error needs to use this stack which is the location of the
2925 + // error. We must restore the stack wherever we handle this
2926 +
2927 // Restore all active ReactContexts to what they were before.
2928 switchContext(previousContext);
2935 - if (__DEV__) {
2936 - task.componentStack = previousComponentStack;
2937 - }
2938 - // We assume that we don't need the correct context.
2939 - // Let's terminate the rest of the tree and don't render any siblings.
2929 +
2930 throw x;
2931 }
2932
@@ -2944,6 +2934,7 @@ function erroredReplay(
2934 request: Request,
2935 boundary: Root | SuspenseBoundary,
2936 error: mixed,
2937 + errorInfo: ThrownInfo,
2938 replayNodes: ReplayNode[],
2939 resumeSlots: ResumeSlots,
2940 ): void {
@@ -2962,11 +2953,11 @@ function erroredReplay(
2953 error.$$typeof === REACT_POSTPONE_TYPE
2954 ) {
2955 const postponeInstance: Postpone = (error: any);
2965 - logPostpone(request, postponeInstance.message);
2956 + logPostpone(request, postponeInstance.message, errorInfo);
2957 // TODO: Figure out a better signal than a magic digest value.
2958 errorDigest = 'POSTPONE';
2959 } else {
2969 - errorDigest = logRecoverableError(request, error);
2960 + errorDigest = logRecoverableError(request, error, errorInfo);
2961 }
2962 abortRemainingReplayNodes(
2963 request,
@@ -2975,6 +2966,7 @@ function erroredReplay(
2966 resumeSlots,
2967 error,
2968 errorDigest,
2969 + errorInfo,
2970 );
2971 }
2972
@@ -2982,6 +2974,7 @@ function erroredTask(
2974 request: Request,
2975 boundary: Root | SuspenseBoundary,
2976 error: mixed,
2977 + errorInfo: ThrownInfo,
2978 ) {
2979 // Report the error to a global handler.
2980 let errorDigest;
@@ -2992,23 +2985,19 @@ function erroredTask(
2985 error.$$typeof === REACT_POSTPONE_TYPE
2986 ) {
2987 const postponeInstance: Postpone = (error: any);
2995 - logPostpone(request, postponeInstance.message);
2988 + logPostpone(request, postponeInstance.message, errorInfo);
2989 // TODO: Figure out a better signal than a magic digest value.
2990 errorDigest = 'POSTPONE';
2991 } else {
2999 - errorDigest = logRecoverableError(request, error);
2992 + errorDigest = logRecoverableError(request, error, errorInfo);
2993 }
2994 if (boundary === null) {
3002 - lastBoundaryErrorComponentStackDev = null;
2995 fatalError(request, error);
2996 } else {
2997 boundary.pendingTasks--;
2998 if (boundary.status !== CLIENT_RENDERED) {
2999 boundary.status = CLIENT_RENDERED;
3008 - boundary.errorDigest = errorDigest;
3009 - if (__DEV__) {
3010 - captureBoundaryErrorDetailsDev(boundary, error);
3011 - }
3000 + encodeErrorForBoundary(boundary, errorDigest, error, errorInfo);
3001
3002 // Regardless of what happens next, this boundary won't be displayed,
3003 // so we can flush it, if the parent already flushed.
@@ -3019,8 +3008,6 @@ function erroredTask(
3008 // We reuse the same queue for errors.
3009 request.clientRenderedBoundaries.push(boundary);
3010 }
3022 - } else {
3023 - lastBoundaryErrorComponentStackDev = null;
3011 }
3012 }
3013
@@ -3048,6 +3035,7 @@ function abortRemainingSuspenseBoundary(
3035 rootSegmentID: number,
3036 error: mixed,
3037 errorDigest: ?string,
3038 + errorInfo: ThrownInfo,
3039 ): void {
3040 const resumedBoundary = createSuspenseBoundary(request, new Set());
3041 resumedBoundary.parentFlushed = true;
@@ -3055,24 +3043,18 @@ function abortRemainingSuspenseBoundary(
3043 resumedBoundary.rootSegmentID = rootSegmentID;
3044
3045 resumedBoundary.status = CLIENT_RENDERED;
3058 - resumedBoundary.errorDigest = errorDigest;
3046 + let errorMessage = error;
3047 if (__DEV__) {
3048 const errorPrefix = 'The server did not finish this Suspense boundary: ';
3061 - let errorMessage;
3049 if (error && typeof error.message === 'string') {
3050 errorMessage = errorPrefix + error.message;
3051 } else {
3052 // eslint-disable-next-line react-internal/safe-string-coercion
3053 errorMessage = errorPrefix + String(error);
3054 }
3068 - const previousTaskInDev = currentTaskInDEV;
3069 - currentTaskInDEV = null;
3070 - try {
3071 - captureBoundaryErrorDetailsDev(resumedBoundary, errorMessage);
3072 - } finally {
3073 - currentTaskInDEV = previousTaskInDev;
3074 - }
3055 }
3056 + encodeErrorForBoundary(resumedBoundary, errorDigest, errorMessage, errorInfo);
3057 +
3058 if (resumedBoundary.parentFlushed) {
3059 request.clientRenderedBoundaries.push(resumedBoundary);
3060 }
@@ -3085,6 +3067,7 @@ function abortRemainingReplayNodes(
3067 slots: ResumeSlots,
3068 error: mixed,
3069 errorDigest: ?string,
3070 + errorInfo: ThrownInfo,
3071 ): void {
3072 for (let i = 0; i < nodes.length; i++) {
3073 const node = nodes[i];
@@ -3096,6 +3079,7 @@ function abortRemainingReplayNodes(
3079 node[3],
3080 error,
3081 errorDigest,
3082 + errorInfo,
3083 );
3084 } else {
3085 const boundaryNode: ReplaySuspenseBoundary = node;
@@ -3105,6 +3089,7 @@ function abortRemainingReplayNodes(
3089 rootSegmentID,
3090 error,
3091 errorDigest,
3092 + errorInfo,
3093 );
3094 }
3095 }
@@ -3121,10 +3106,7 @@ function abortRemainingReplayNodes(
3106 );
3107 } else if (boundary.status !== CLIENT_RENDERED) {
3108 boundary.status = CLIENT_RENDERED;
3124 - boundary.errorDigest = errorDigest;
3125 - if (__DEV__) {
3126 - captureBoundaryErrorDetailsDev(boundary, error);
3127 - }
3109 + encodeErrorForBoundary(boundary, errorDigest, error, errorInfo);
3110 if (boundary.parentFlushed) {
3111 request.clientRenderedBoundaries.push(boundary);
3112 }
@@ -3148,12 +3130,13 @@ function abortTask(task: Task, request: Request, error: mixed): void {
3130 }
3131
3132 if (boundary === null) {
3133 + const errorInfo: ThrownInfo = {};
3134 if (request.status !== CLOSING && request.status !== CLOSED) {
3135 const replay: null | ReplaySet = task.replay;
3136 if (replay === null) {
3137 // We didn't complete the root so we have nothing to show. We can close
3138 // the request;
3156 - logRecoverableError(request, error);
3139 + logRecoverableError(request, error, errorInfo);
3140 fatalError(request, error);
3141 return;
3142 } else {
@@ -3162,7 +3145,7 @@ function abortTask(task: Task, request: Request, error: mixed): void {
3145 // the ReplaySet.
3146 replay.pendingTasks--;
3147 if (replay.pendingTasks === 0 && replay.nodes.length > 0) {
3165 - const errorDigest = logRecoverableError(request, error);
3148 + const errorDigest = logRecoverableError(request, error, errorInfo);
3149 abortRemainingReplayNodes(
3150 request,
3151 null,
@@ -3170,6 +3153,7 @@ function abortTask(task: Task, request: Request, error: mixed): void {
3153 replay.slots,
3154 error,
3155 errorDigest,
3156 + errorInfo,
3157 );
3158 }
3159 request.pendingRootTasks--;
@@ -3182,25 +3166,23 @@ function abortTask(task: Task, request: Request, error: mixed): void {
3166 boundary.pendingTasks--;
3167 if (boundary.status !== CLIENT_RENDERED) {
3168 boundary.status = CLIENT_RENDERED;
3185 - boundary.errorDigest = logRecoverableError(request, error);
3169 + // We construct an errorInfo from the boundary's componentStack so the error in dev will indicate which
3170 + // boundary the message is referring to
3171 + const errorInfo = getThrownInfo(task.componentStack);
3172 + const errorDigest = logRecoverableError(request, error, errorInfo);
3173 + let errorMessage = error;
3174 if (__DEV__) {
3175 const errorPrefix =
3176 'The server did not finish this Suspense boundary: ';
3189 - let errorMessage;
3177 if (error && typeof error.message === 'string') {
3178 errorMessage = errorPrefix + error.message;
3179 } else {
3180 // eslint-disable-next-line react-internal/safe-string-coercion
3181 errorMessage = errorPrefix + String(error);
3182 }
3196 - const previousTaskInDev = currentTaskInDEV;
3197 - currentTaskInDEV = task;
3198 - try {
3199 - captureBoundaryErrorDetailsDev(boundary, errorMessage);
3200 - } finally {
3201 - currentTaskInDEV = previousTaskInDev;
3202 - }
3183 }
3184 + encodeErrorForBoundary(boundary, errorDigest, errorMessage, errorInfo);
3185 +
3186 if (boundary.parentFlushed) {
3187 request.clientRenderedBoundaries.push(boundary);
3188 }
@@ -3232,7 +3214,8 @@ function safelyEmitEarlyPreloads(
3214 );
3215 } catch (error) {
3216 // We assume preloads are optimistic and thus non-fatal if errored.
3235 - logRecoverableError(request, error);
3217 + const errorInfo: ThrownInfo = {};
3218 + logRecoverableError(request, error, errorInfo);
3219 }
3220 }
3221
@@ -3486,16 +3469,19 @@ function retryRenderTask(
3469 const trackedPostpones = request.trackedPostpones;
3470 task.abortSet.delete(task);
3471 const postponeInstance: Postpone = (x: any);
3489 - logPostpone(request, postponeInstance.message);
3472 +
3473 + const postponeInfo = getThrownInfo(task.componentStack);
3474 + logPostpone(request, postponeInstance.message, postponeInfo);
3475 trackPostpone(request, trackedPostpones, task, segment);
3476 finishedTask(request, task.blockedBoundary, segment);
3492 - lastBoundaryErrorComponentStackDev = null;
3477 return;
3478 }
3479 }
3480 +
3481 + const errorInfo = getThrownInfo(task.componentStack);
3482 task.abortSet.delete(task);
3483 segment.status = ERRORED;
3498 - erroredTask(request, task.blockedBoundary, x);
3484 + erroredTask(request, task.blockedBoundary, x, errorInfo);
3485 return;
3486 } finally {
3487 if (enableFloat) {
@@ -3576,10 +3562,12 @@ function retryReplayTask(request: Request, task: ReplayTask): void {
3562 }
3563 task.replay.pendingTasks--;
3564 task.abortSet.delete(task);
3565 + const errorInfo = getThrownInfo(task.componentStack);
3566 erroredReplay(
3567 request,
3568 task.blockedBoundary,
3569 x,
3570 + errorInfo,
3571 task.replay.nodes,
3572 task.replay.slots,
3573 );
@@ -3637,7 +3625,8 @@ export function performWork(request: Request): void {
3625 flushCompletedQueues(request, request.destination);
3626 }
3627 } catch (error) {
3640 - logRecoverableError(request, error);
3628 + const errorInfo: ThrownInfo = {};
3629 + logRecoverableError(request, error, errorInfo);
3630 fatalError(request, error);
3631 } finally {
3632 setCurrentResumableState(prevResumableState);
@@ -4201,7 +4190,8 @@ export function startFlowing(request: Request, destination: Destination): void {
4190 try {
4191 flushCompletedQueues(request, destination);
4192 } catch (error) {
4204 - logRecoverableError(request, error);
4193 + const errorInfo: ThrownInfo = {};
4194 + logRecoverableError(request, error, errorInfo);
4195 fatalError(request, error);
4196 }
4197 }
@@ -4226,7 +4216,8 @@ export function abort(request: Request, reason: mixed): void {
4216 flushCompletedQueues(request, request.destination);
4217 }
4218 } catch (error) {
4229 - logRecoverableError(request, error);
4219 + const errorInfo: ThrownInfo = {};
4220 + logRecoverableError(request, error, errorInfo);
4221 fatalError(request, error);
4222 }
4223 }