Skip to content

Commit 383b2a1

Browse files
authored
Use the Nearest Parent of an Errored Promise as its Owner (#29814)
Stacked on #29807. Conceptually the error's owner/task should ideally be captured when the Error constructor is called but neither `console.createTask` does this, nor do we override `Error` to capture our `owner`. So instead, we use the nearest parent as the owner/task of the error. This is usually the same thing when it's thrown from the same async component but not if you await a promise started from a different component/task. Before this stack the "owner" and "task" of a Lazy that errors was the nearest Fiber but if the thing erroring is a Server Component, we need to get that as the owner from the inner most part of debugInfo. To get the Task for that Server Component, we need to expose it on the ReactComponentInfo object. Unfortunately that makes the object not serializable so we need to special case this to exclude it from serialization. It gets restored again on the client. Before (Shell): <img width="813" alt="Screenshot 2024-06-06 at 5 16 20 PM" src="https://github.com/facebook/react/assets/63648/7da2d4c9-539b-494e-ba63-1abdc58ff13c"> After (App): <img width="811" alt="Screenshot 2024-06-08 at 12 29 23 AM" src="https://github.com/facebook/react/assets/63648/dbf40bd7-c24d-4200-81a6-5018bef55f6d">
1 parent a26e3f4 commit 383b2a1

File tree

7 files changed

+105
-15
lines changed

7 files changed

+105
-15
lines changed

.eslintrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ module.exports = {
245245
},
246246
],
247247
'no-shadow': ERROR,
248-
'no-unused-vars': [ERROR, {args: 'none'}],
248+
'no-unused-vars': [ERROR, {args: 'none', ignoreRestSiblings: true}],
249249
'no-use-before-define': OFF,
250250
'no-useless-concat': OFF,
251251
quotes: [ERROR, 'single', {avoidEscape: true, allowTemplateLiterals: true}],

packages/react-client/src/ReactFlightClient.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ type InitializedStreamChunk<
162162
value: T,
163163
reason: FlightStreamController,
164164
_response: Response,
165+
_debugInfo?: null | ReactDebugInfo,
165166
then(resolve: (ReadableStream) => mixed, reject?: (mixed) => mixed): void,
166167
};
167168
type ErroredChunk<T> = {
@@ -1710,11 +1711,6 @@ function resolveHint<Code: HintCode>(
17101711
const supportsCreateTask =
17111712
__DEV__ && enableOwnerStacks && !!(console: any).createTask;
17121713

1713-
const taskCache: null | WeakMap<
1714-
ReactComponentInfo | ReactAsyncInfo,
1715-
ConsoleTask,
1716-
> = supportsCreateTask ? new WeakMap() : null;
1717-
17181714
type FakeFunction<T> = (() => T) => T;
17191715
const fakeFunctionCache: Map<string, FakeFunction<any>> = __DEV__
17201716
? new Map()
@@ -1834,12 +1830,12 @@ function initializeFakeTask(
18341830
response: Response,
18351831
debugInfo: ReactComponentInfo | ReactAsyncInfo,
18361832
): null | ConsoleTask {
1837-
if (taskCache === null || typeof debugInfo.stack !== 'string') {
1833+
if (!supportsCreateTask || typeof debugInfo.stack !== 'string') {
18381834
return null;
18391835
}
18401836
const componentInfo: ReactComponentInfo = (debugInfo: any); // Refined
18411837
const stack: string = debugInfo.stack;
1842-
const cachedEntry = taskCache.get((componentInfo: any));
1838+
const cachedEntry = componentInfo.task;
18431839
if (cachedEntry !== undefined) {
18441840
return cachedEntry;
18451841
}
@@ -1856,16 +1852,20 @@ function initializeFakeTask(
18561852
);
18571853
const callStack = buildFakeCallStack(response, stack, createTaskFn);
18581854

1855+
let componentTask;
18591856
if (ownerTask === null) {
18601857
const rootTask = response._debugRootTask;
18611858
if (rootTask != null) {
1862-
return rootTask.run(callStack);
1859+
componentTask = rootTask.run(callStack);
18631860
} else {
1864-
return callStack();
1861+
componentTask = callStack();
18651862
}
18661863
} else {
1867-
return ownerTask.run(callStack);
1864+
componentTask = ownerTask.run(callStack);
18681865
}
1866+
// $FlowFixMe[cannot-write]: We consider this part of initialization.
1867+
componentInfo.task = componentTask;
1868+
return componentTask;
18691869
}
18701870

18711871
function resolveDebugInfo(

packages/react-client/src/__tests__/ReactFlight-test.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,27 @@ function normalizeCodeLocInfo(str) {
2727
);
2828
}
2929

30+
function normalizeComponentInfo(debugInfo) {
31+
if (typeof debugInfo.stack === 'string') {
32+
const {task, ...copy} = debugInfo;
33+
copy.stack = normalizeCodeLocInfo(debugInfo.stack);
34+
if (debugInfo.owner) {
35+
copy.owner = normalizeComponentInfo(debugInfo.owner);
36+
}
37+
return copy;
38+
} else {
39+
return debugInfo;
40+
}
41+
}
42+
3043
function getDebugInfo(obj) {
3144
const debugInfo = obj._debugInfo;
3245
if (debugInfo) {
46+
const copy = [];
3347
for (let i = 0; i < debugInfo.length; i++) {
34-
if (typeof debugInfo[i].stack === 'string') {
35-
debugInfo[i].stack = normalizeCodeLocInfo(debugInfo[i].stack);
36-
}
48+
copy.push(normalizeComponentInfo(debugInfo[i]));
3749
}
50+
return copy;
3851
}
3952
return debugInfo;
4053
}

packages/react-reconciler/src/ReactChildFiber.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import {
4848
enableRefAsProp,
4949
enableAsyncIterableChildren,
5050
disableLegacyMode,
51+
enableOwnerStacks,
5152
} from 'shared/ReactFeatureFlags';
5253

5354
import {
@@ -1959,7 +1960,28 @@ function createChildReconciler(
19591960
const throwFiber = createFiberFromThrow(x, returnFiber.mode, lanes);
19601961
throwFiber.return = returnFiber;
19611962
if (__DEV__) {
1962-
throwFiber._debugInfo = currentDebugInfo;
1963+
const debugInfo = (throwFiber._debugInfo = currentDebugInfo);
1964+
// Conceptually the error's owner/task should ideally be captured when the
1965+
// Error constructor is called but neither console.createTask does this,
1966+
// nor do we override them to capture our `owner`. So instead, we use the
1967+
// nearest parent as the owner/task of the error. This is usually the same
1968+
// thing when it's thrown from the same async component but not if you await
1969+
// a promise started from a different component/task.
1970+
throwFiber._debugOwner = returnFiber._debugOwner;
1971+
if (enableOwnerStacks) {
1972+
throwFiber._debugTask = returnFiber._debugTask;
1973+
}
1974+
if (debugInfo != null) {
1975+
for (let i = debugInfo.length - 1; i >= 0; i--) {
1976+
if (typeof debugInfo[i].stack === 'string') {
1977+
throwFiber._debugOwner = (debugInfo[i]: any);
1978+
if (enableOwnerStacks) {
1979+
throwFiber._debugTask = debugInfo[i].task;
1980+
}
1981+
break;
1982+
}
1983+
}
1984+
}
19631985
}
19641986
return throwFiber;
19651987
} finally {

packages/react-reconciler/src/getComponentNameFromFiber.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
LegacyHiddenComponent,
4545
CacheComponent,
4646
TracingMarkerComponent,
47+
Throw,
4748
} from 'react-reconciler/src/ReactWorkTags';
4849
import getComponentNameFromType from 'shared/getComponentNameFromType';
4950
import {REACT_STRICT_MODE_TYPE} from 'shared/ReactSymbols';
@@ -160,6 +161,26 @@ export default function getComponentNameFromFiber(fiber: Fiber): string | null {
160161
if (enableLegacyHidden) {
161162
return 'LegacyHidden';
162163
}
164+
break;
165+
case Throw: {
166+
if (__DEV__) {
167+
// For an error in child position we use the of the inner most parent component.
168+
// Whether a Server Component or the parent Fiber.
169+
const debugInfo = fiber._debugInfo;
170+
if (debugInfo != null) {
171+
for (let i = debugInfo.length - 1; i >= 0; i--) {
172+
if (typeof debugInfo[i].name === 'string') {
173+
return debugInfo[i].name;
174+
}
175+
}
176+
}
177+
if (fiber.return === null) {
178+
return null;
179+
}
180+
return getComponentNameFromFiber(fiber.return);
181+
}
182+
return null;
183+
}
163184
}
164185

165186
return null;

packages/react-server/src/ReactFlightServer.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ export type ReactClientValue =
352352
// subtype, so the receiver can only accept once of these.
353353
| React$Element<string>
354354
| React$Element<ClientReference<any> & any>
355+
| ReactComponentInfo
355356
| string
356357
| boolean
357358
| number
@@ -2462,6 +2463,32 @@ function renderModelDestructive(
24622463
);
24632464
}
24642465
if (__DEV__) {
2466+
if (
2467+
// TODO: We don't currently have a brand check on ReactComponentInfo. Reconsider.
2468+
typeof value.task === 'object' &&
2469+
value.task !== null &&
2470+
// $FlowFixMe[method-unbinding]
2471+
typeof value.task.run === 'function' &&
2472+
typeof value.name === 'string' &&
2473+
typeof value.env === 'string' &&
2474+
value.owner !== undefined &&
2475+
(enableOwnerStacks
2476+
? typeof (value: any).stack === 'string'
2477+
: typeof (value: any).stack === 'undefined')
2478+
) {
2479+
// This looks like a ReactComponentInfo. We can't serialize the ConsoleTask object so we
2480+
// need to omit it before serializing.
2481+
const componentDebugInfo = {
2482+
name: value.name,
2483+
env: value.env,
2484+
owner: value.owner,
2485+
};
2486+
if (enableOwnerStacks) {
2487+
(componentDebugInfo: any).stack = (value: any).stack;
2488+
}
2489+
return (componentDebugInfo: any);
2490+
}
2491+
24652492
if (objectName(value) !== 'Object') {
24662493
console.error(
24672494
'Only plain objects can be passed to Client Components from Server Components. ' +
@@ -3231,6 +3258,12 @@ function forwardDebugInfo(
32313258
) {
32323259
for (let i = 0; i < debugInfo.length; i++) {
32333260
request.pendingChunks++;
3261+
if (typeof debugInfo[i].name === 'string') {
3262+
// We outline this model eagerly so that we can refer to by reference as an owner.
3263+
// If we had a smarter way to dedupe we might not have to do this if there ends up
3264+
// being no references to this as an owner.
3265+
outlineModel(request, debugInfo[i]);
3266+
}
32343267
emitDebugChunk(request, id, debugInfo[i]);
32353268
}
32363269
}

packages/shared/ReactTypes.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ export type ReactComponentInfo = {
183183
+env?: string,
184184
+owner?: null | ReactComponentInfo,
185185
+stack?: null | string,
186+
+task?: null | ConsoleTask,
186187
};
187188

188189
export type ReactAsyncInfo = {

0 commit comments

Comments
 (0)