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
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
// 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
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
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
emptyContextObject,
428
rootContextSnapshot,
429
emptyTreeContext,
430
+ null,
431
);
432
pingedTasks.push(rootTask);
433
return request;
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,
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> = [];
538
emptyContextObject,
539
rootContextSnapshot,
540
emptyTreeContext,
541
+ null,
542
);
543
pingedTasks.push(rootTask);
544
return request;
562
emptyContextObject,
563
rootContextSnapshot,
564
emptyTreeContext,
565
+ null,
566
);
567
pingedTasks.push(rootTask);
568
return request;
620
legacyContext: LegacyContext,
621
context: ContextSnapshot,
622
treeContext: TreeContext,
623
+ componentStack: null | ComponentStackNode,
624
): RenderTask {
625
request.allPendingTasks++;
626
if (blockedBoundary === null) {
628
} else {
629
blockedBoundary.pendingTasks++;
630
}
627
- const task: RenderTask = ({
631
+ const task: RenderTask = {
632
replay: null,
633
node,
634
childIndex,
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
}
661
legacyContext: LegacyContext,
662
context: ContextSnapshot,
663
treeContext: TreeContext,
664
+ componentStack: null | ComponentStackNode,
665
): ReplayTask {
666
request.allPendingTasks++;
667
if (blockedBoundary === null) {
670
blockedBoundary.pendingTasks++;
671
}
672
replay.pendingTasks++;
670
- const task: ReplayTask = ({
673
+ const task: ReplayTask = {
674
replay,
675
node,
676
childIndex,
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
}
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;
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
}
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;
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);
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 &&
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.
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]];
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(
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;
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. " +
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 &&
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
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]];
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(
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;
1209
pushEndCompletedSuspenseBoundary(segment.chunks);
1210
}
1211
task.keyPath = prevKeyPath;
1175
-
1176
- popComponentStackInDEV(task);
1212
+ task.componentStack = previousComponentStack;
1213
}
1214
1215
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
1272
);
1273
segment.lastPushedText = false;
1274
}
1238
- popComponentStackInDEV(task);
1275
+ task.componentStack = previousComponentStack;
1276
}
1277
1278
function shouldConstruct(Component: any) {
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} = {};
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 (
1501
formStateMatchingIndex,
1502
);
1503
}
1465
- popComponentStackInDEV(task);
1504
+ task.componentStack = previousComponentStack;
1505
}
1506
1507
function finishFunctionComponent(
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,
1663
formStateCount,
1664
formStateMatchingIndex,
1665
);
1626
- popComponentStackInDEV(task);
1666
+ task.componentStack = previousComponentStack;
1667
}
1668
1669
function renderMemo(
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);
1795
resolvedProps,
1796
ref,
1797
);
1757
- popComponentStackInDEV(task);
1798
+ task.componentStack = previousComponentStack;
1799
}
1800
1801
function renderOffscreen(
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: {
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 {
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,
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.
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
}
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,
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,
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
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;
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
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
}
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.
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
}
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
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;
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
2934
request: Request,
2935
boundary: Root | SuspenseBoundary,
2936
error: mixed,
2937
+ errorInfo: ThrownInfo,
2938
replayNodes: ReplayNode[],
2939
resumeSlots: ResumeSlots,
2940
): void {
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,
2966
resumeSlots,
2967
error,
2968
errorDigest,
2969
+ errorInfo,
2970
);
2971
}
2972
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;
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.
3008
// We reuse the same queue for errors.
3009
request.clientRenderedBoundaries.push(boundary);
3010
}
3022
- } else {
3023
- lastBoundaryErrorComponentStackDev = null;
3011
}
3012
}
3013
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;
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
}
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];
3079
node[3],
3080
error,
3081
errorDigest,
3082
+ errorInfo,
3083
);
3084
} else {
3085
const boundaryNode: ReplaySuspenseBoundary = node;
3089
rootSegmentID,
3090
error,
3091
errorDigest,
3092
+ errorInfo,
3093
);
3094
}
3095
}
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
}
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 {
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,
3153
replay.slots,
3154
error,
3155
errorDigest,
3156
+ errorInfo,
3157
);
3158
}
3159
request.pendingRootTasks--;
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
}
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
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) {
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
);
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);
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
}
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
}