Skip to content

Commit 20e5431

Browse files
authored
[Flight][Fiber] Encode owner in the error payload in dev and use it as the Error's Task (#34460)
When we report an error we typically log the owner stack of the thing that caught the error. Similarly we restore the `console.createTask` scope of the catching component when we call `reportError` or `console.error`. We also have a special case if something throws during reconciliation which uses the Server Component task as far as we got before we threw. https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactChildFiber.js#L1952-L1960 Chrome has since fixed it (on our request) that the Error constructor snapshots the Task at the time the constructor was created and logs that in `reportError`. This is a good thing since it means we get a coherent stack. Unfortunately, it means that the fake Errors that we create in Flight Client gets a snapshot of the task where they were created so when they're reported in the console they get the root Task instead of the Task of the handler of the error. Ideally we'd transfer the Task from the server and restore it. However, since we don't instrument the Error object to snapshot the owner and we can't read the native Task (if it's even enabled on the server) we don't actually have a correct snapshot to transfer for a Server Component Error. However, we can use the parent's task for where the error was observed by Flight Server and then encode that as a pseudo owner of the Error. Then we use this owner as the Task which the Error is created within. Now the client snapshots that Task which is reported by `reportError` so now we have an async stack for Server Component errors again. (Note that this owner may differ from the one observed by `captureOwnerStack` which gets the nearest Server Component from where it was caught. We could attach the owner to the Error object and use that owner when calling `onCaughtError`/`onUncaughtError`). Before: <img width="911" height="57" alt="Screenshot 2025-09-10 at 10 57 54 AM" src="https://github.com/user-attachments/assets/0446ef96-fad9-4e17-8a9a-d89c334233ec" /> After: <img width="910" height="128" alt="Screenshot 2025-09-10 at 11 06 20 AM" src="https://github.com/user-attachments/assets/b30e5892-cf40-4246-a588-0f309575439b" /> Similarly, there are Errors and warnings created by ChildFiber itself. Those execute in the scope of the general render of the parent Fiber. They used to get the scope of the nearest client component parent (e.g. div in this case) but that's the parent of the Server Component. It would be too expensive to run every level of reconciliation in its own task optimistically, so this does it only when we know that we'll throw or log an error that needs this context. Unfortunately this doesn't cover user space errors (such as if an iterable errors). Before: <img width="903" height="298" alt="Screenshot 2025-09-10 at 11 31 55 AM" src="https://github.com/user-attachments/assets/cffc94da-8c14-4d6e-9a5b-bf0833b8b762" /> After: <img width="1216" height="252" alt="Screenshot 2025-09-10 at 11 50 54 AM" src="https://github.com/user-attachments/assets/f85f93cf-ab73-4046-af3d-dd93b73b3552" /> <img width="412" height="115" alt="Screenshot 2025-09-10 at 11 52 46 AM" src="https://github.com/user-attachments/assets/a76cef7b-b162-4ecf-9b0a-68bf34afc239" />
1 parent 1a27af3 commit 20e5431

File tree

4 files changed

+114
-27
lines changed

4 files changed

+114
-27
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3181,11 +3181,27 @@ function resolveErrorDev(
31813181
'An error occurred in the Server Components render but no message was provided',
31823182
),
31833183
);
3184-
const rootTask = getRootTask(response, env);
3185-
if (rootTask != null) {
3186-
error = rootTask.run(callStack);
3184+
3185+
let ownerTask: null | ConsoleTask = null;
3186+
if (errorInfo.owner != null) {
3187+
const ownerRef = errorInfo.owner.slice(1);
3188+
// TODO: This is not resilient to the owner loading later in an Error like a debug channel.
3189+
// The whole error serialization should probably go through the regular model at least for DEV.
3190+
const owner = getOutlinedModel(response, ownerRef, {}, '', createModel);
3191+
if (owner !== null) {
3192+
ownerTask = initializeFakeTask(response, owner);
3193+
}
3194+
}
3195+
3196+
if (ownerTask === null) {
3197+
const rootTask = getRootTask(response, env);
3198+
if (rootTask != null) {
3199+
error = rootTask.run(callStack);
3200+
} else {
3201+
error = callStack();
3202+
}
31873203
} else {
3188-
error = callStack();
3204+
error = ownerTask.run(callStack);
31893205
}
31903206

31913207
(error: any).name = name;

packages/react-reconciler/src/ReactChildFiber.js

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
Thenable,
1414
ReactContext,
1515
ReactDebugInfo,
16+
ReactComponentInfo,
1617
SuspenseListRevealOrder,
1718
} from 'shared/ReactTypes';
1819
import type {Fiber} from './ReactInternalTypes';
@@ -101,6 +102,25 @@ function pushDebugInfo(
101102
return previousDebugInfo;
102103
}
103104

105+
function getCurrentDebugTask(): null | ConsoleTask {
106+
// Get the debug task of the parent Server Component if there is one.
107+
if (__DEV__) {
108+
const debugInfo = currentDebugInfo;
109+
if (debugInfo != null) {
110+
for (let i = debugInfo.length - 1; i >= 0; i--) {
111+
if (debugInfo[i].name != null) {
112+
const componentInfo: ReactComponentInfo = debugInfo[i];
113+
const debugTask: ?ConsoleTask = componentInfo.debugTask;
114+
if (debugTask != null) {
115+
return debugTask;
116+
}
117+
}
118+
}
119+
}
120+
}
121+
return null;
122+
}
123+
104124
let didWarnAboutMaps;
105125
let didWarnAboutGenerators;
106126
let ownerHasKeyUseWarning;
@@ -274,7 +294,7 @@ function coerceRef(workInProgress: Fiber, element: ReactElement): void {
274294
workInProgress.ref = refProp !== undefined ? refProp : null;
275295
}
276296

277-
function throwOnInvalidObjectType(returnFiber: Fiber, newChild: Object) {
297+
function throwOnInvalidObjectTypeImpl(returnFiber: Fiber, newChild: Object) {
278298
if (newChild.$$typeof === REACT_LEGACY_ELEMENT_TYPE) {
279299
throw new Error(
280300
'A React Element from an older version of React was rendered. ' +
@@ -299,7 +319,18 @@ function throwOnInvalidObjectType(returnFiber: Fiber, newChild: Object) {
299319
);
300320
}
301321

302-
function warnOnFunctionType(returnFiber: Fiber, invalidChild: Function) {
322+
function throwOnInvalidObjectType(returnFiber: Fiber, newChild: Object) {
323+
const debugTask = getCurrentDebugTask();
324+
if (__DEV__ && debugTask !== null) {
325+
debugTask.run(
326+
throwOnInvalidObjectTypeImpl.bind(null, returnFiber, newChild),
327+
);
328+
} else {
329+
throwOnInvalidObjectTypeImpl(returnFiber, newChild);
330+
}
331+
}
332+
333+
function warnOnFunctionTypeImpl(returnFiber: Fiber, invalidChild: Function) {
303334
if (__DEV__) {
304335
const parentName = getComponentNameFromFiber(returnFiber) || 'Component';
305336

@@ -336,7 +367,16 @@ function warnOnFunctionType(returnFiber: Fiber, invalidChild: Function) {
336367
}
337368
}
338369

339-
function warnOnSymbolType(returnFiber: Fiber, invalidChild: symbol) {
370+
function warnOnFunctionType(returnFiber: Fiber, invalidChild: Function) {
371+
const debugTask = getCurrentDebugTask();
372+
if (__DEV__ && debugTask !== null) {
373+
debugTask.run(warnOnFunctionTypeImpl.bind(null, returnFiber, invalidChild));
374+
} else {
375+
warnOnFunctionTypeImpl(returnFiber, invalidChild);
376+
}
377+
}
378+
379+
function warnOnSymbolTypeImpl(returnFiber: Fiber, invalidChild: symbol) {
340380
if (__DEV__) {
341381
const parentName = getComponentNameFromFiber(returnFiber) || 'Component';
342382

@@ -364,6 +404,15 @@ function warnOnSymbolType(returnFiber: Fiber, invalidChild: symbol) {
364404
}
365405
}
366406

407+
function warnOnSymbolType(returnFiber: Fiber, invalidChild: symbol) {
408+
const debugTask = getCurrentDebugTask();
409+
if (__DEV__ && debugTask !== null) {
410+
debugTask.run(warnOnSymbolTypeImpl.bind(null, returnFiber, invalidChild));
411+
} else {
412+
warnOnSymbolTypeImpl(returnFiber, invalidChild);
413+
}
414+
}
415+
367416
type ChildReconciler = (
368417
returnFiber: Fiber,
369418
currentFirstChild: Fiber | null,
@@ -1941,12 +1990,14 @@ function createChildReconciler(
19411990
throwFiber.return = returnFiber;
19421991
if (__DEV__) {
19431992
const debugInfo = (throwFiber._debugInfo = currentDebugInfo);
1944-
// Conceptually the error's owner/task should ideally be captured when the
1945-
// Error constructor is called but neither console.createTask does this,
1946-
// nor do we override them to capture our `owner`. So instead, we use the
1947-
// nearest parent as the owner/task of the error. This is usually the same
1948-
// thing when it's thrown from the same async component but not if you await
1949-
// a promise started from a different component/task.
1993+
// Conceptually the error's owner should ideally be captured when the
1994+
// Error constructor is called but we don't override them to capture our
1995+
// `owner`. So instead, we use the nearest parent as the owner/task of the
1996+
// error. This is usually the same thing when it's thrown from the same
1997+
// async component but not if you await a promise started from a different
1998+
// component/task.
1999+
// In newer Chrome, Error constructor does capture the Task which is what
2000+
// is logged by reportError. In that case this debugTask isn't used.
19502001
throwFiber._debugOwner = returnFiber._debugOwner;
19512002
throwFiber._debugTask = returnFiber._debugTask;
19522003
if (debugInfo != null) {

packages/react-server/src/ReactFlightServer.js

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -864,7 +864,7 @@ function serializeDebugThenable(
864864
const x = thenable.reason;
865865
// We don't log these errors since they didn't actually throw into Flight.
866866
const digest = '';
867-
emitErrorChunk(request, id, digest, x, true);
867+
emitErrorChunk(request, id, digest, x, true, null);
868868
return ref;
869869
}
870870
}
@@ -916,7 +916,7 @@ function serializeDebugThenable(
916916
}
917917
// We don't log these errors since they didn't actually throw into Flight.
918918
const digest = '';
919-
emitErrorChunk(request, id, digest, reason, true);
919+
emitErrorChunk(request, id, digest, reason, true, null);
920920
enqueueFlush(request);
921921
},
922922
);
@@ -964,7 +964,7 @@ function emitRequestedDebugThenable(
964964
}
965965
// We don't log these errors since they didn't actually throw into Flight.
966966
const digest = '';
967-
emitErrorChunk(request, id, digest, reason, true);
967+
emitErrorChunk(request, id, digest, reason, true, null);
968968
enqueueFlush(request);
969969
},
970970
);
@@ -2764,7 +2764,7 @@ function serializeClientReference(
27642764
request.pendingChunks++;
27652765
const errorId = request.nextChunkId++;
27662766
const digest = logRecoverableError(request, x, null);
2767-
emitErrorChunk(request, errorId, digest, x, false);
2767+
emitErrorChunk(request, errorId, digest, x, false, null);
27682768
return serializeByValueID(errorId);
27692769
}
27702770
}
@@ -2813,7 +2813,7 @@ function serializeDebugClientReference(
28132813
request.pendingDebugChunks++;
28142814
const errorId = request.nextChunkId++;
28152815
const digest = logRecoverableError(request, x, null);
2816-
emitErrorChunk(request, errorId, digest, x, true);
2816+
emitErrorChunk(request, errorId, digest, x, true, null);
28172817
return serializeByValueID(errorId);
28182818
}
28192819
}
@@ -3054,7 +3054,7 @@ function serializeDebugBlob(request: Request, blob: Blob): string {
30543054
}
30553055
function error(reason: mixed) {
30563056
const digest = '';
3057-
emitErrorChunk(request, id, digest, reason, true);
3057+
emitErrorChunk(request, id, digest, reason, true, null);
30583058
enqueueFlush(request);
30593059
// $FlowFixMe should be able to pass mixed
30603060
reader.cancel(reason).then(noop, noop);
@@ -3254,7 +3254,14 @@ function renderModel(
32543254
emitPostponeChunk(request, errorId, postponeInstance);
32553255
} else {
32563256
const digest = logRecoverableError(request, x, task);
3257-
emitErrorChunk(request, errorId, digest, x, false);
3257+
emitErrorChunk(
3258+
request,
3259+
errorId,
3260+
digest,
3261+
x,
3262+
false,
3263+
__DEV__ ? task.debugOwner : null,
3264+
);
32583265
}
32593266
if (wasReactNode) {
32603267
// We'll replace this element with a lazy reference that throws on the client
@@ -4072,7 +4079,8 @@ function emitErrorChunk(
40724079
id: number,
40734080
digest: string,
40744081
error: mixed,
4075-
debug: boolean,
4082+
debug: boolean, // DEV-only
4083+
owner: ?ReactComponentInfo, // DEV-only
40764084
): void {
40774085
let errorInfo: ReactErrorInfo;
40784086
if (__DEV__) {
@@ -4104,7 +4112,9 @@ function emitErrorChunk(
41044112
message = 'An error occurred but serializing the error message failed.';
41054113
stack = [];
41064114
}
4107-
errorInfo = {digest, name, message, stack, env};
4115+
const ownerRef =
4116+
owner == null ? null : outlineComponentInfo(request, owner);
4117+
errorInfo = {digest, name, message, stack, env, owner: ownerRef};
41084118
} else {
41094119
errorInfo = {digest};
41104120
}
@@ -4204,7 +4214,7 @@ function emitDebugChunk(
42044214
function outlineComponentInfo(
42054215
request: Request,
42064216
componentInfo: ReactComponentInfo,
4207-
): void {
4217+
): string {
42084218
if (!__DEV__) {
42094219
// These errors should never make it into a build so we don't need to encode them in codes.json
42104220
// eslint-disable-next-line react-internal/prod-error-codes
@@ -4213,9 +4223,10 @@ function outlineComponentInfo(
42134223
);
42144224
}
42154225

4216-
if (request.writtenDebugObjects.has(componentInfo)) {
4226+
const existingRef = request.writtenDebugObjects.get(componentInfo);
4227+
if (existingRef !== undefined) {
42174228
// Already written
4218-
return;
4229+
return existingRef;
42194230
}
42204231

42214232
if (componentInfo.owner != null) {
@@ -4270,6 +4281,7 @@ function outlineComponentInfo(
42704281
request.writtenDebugObjects.set(componentInfo, ref);
42714282
// We also store this in the main dedupe set so that it can be referenced by inline React Elements.
42724283
request.writtenObjects.set(componentInfo, ref);
4284+
return ref;
42734285
}
42744286

42754287
function emitIOInfoChunk(
@@ -5465,7 +5477,14 @@ function erroredTask(request: Request, task: Task, error: mixed): void {
54655477
emitPostponeChunk(request, task.id, postponeInstance);
54665478
} else {
54675479
const digest = logRecoverableError(request, error, task);
5468-
emitErrorChunk(request, task.id, digest, error, false);
5480+
emitErrorChunk(
5481+
request,
5482+
task.id,
5483+
digest,
5484+
error,
5485+
false,
5486+
__DEV__ ? task.debugOwner : null,
5487+
);
54695488
}
54705489
request.abortableTasks.delete(task);
54715490
callOnAllReadyIfReady(request);
@@ -6040,7 +6059,7 @@ export function abort(request: Request, reason: mixed): void {
60406059
const errorId = request.nextChunkId++;
60416060
request.fatalError = errorId;
60426061
request.pendingChunks++;
6043-
emitErrorChunk(request, errorId, digest, error, false);
6062+
emitErrorChunk(request, errorId, digest, error, false, null);
60446063
abortableTasks.forEach(task => abortTask(task, request, errorId));
60456064
scheduleWork(() => finishAbort(request, abortableTasks, errorId));
60466065
}

packages/shared/ReactTypes.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ export type ReactErrorInfoDev = {
228228
+message: string,
229229
+stack: ReactStackTrace,
230230
+env: string,
231+
+owner?: null | string,
231232
};
232233

233234
export type ReactErrorInfo = ReactErrorInfoProd | ReactErrorInfoDev;

0 commit comments

Comments
 (0)