@samitouri / QOS-React / commits / 20e5431747

[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" />

Sebastian Markbåge committed Sep 12, 2025 at 11:55 UTC 20e5431747347796b3be8312e56cef655b26ef4d
4 files changed +114 -27
packages/react-client/src/ReactFlightClient.js
+20 -4
@@ -3181,11 +3181,27 @@ function resolveErrorDev(
3181 'An error occurred in the Server Components render but no message was provided',
3182 ),
3183 );
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 + }
3203 } else {
3188 - error = callStack();
3204 + error = ownerTask.run(callStack);
3205 }
3206
3207 (error: any).name = name;
packages/react-reconciler/src/ReactChildFiber.js
+60 -9
@@ -13,6 +13,7 @@ import type {
13 Thenable,
14 ReactContext,
15 ReactDebugInfo,
16 + ReactComponentInfo,
17 SuspenseListRevealOrder,
18 } from 'shared/ReactTypes';
19 import type {Fiber} from './ReactInternalTypes';
@@ -101,6 +102,25 @@ function pushDebugInfo(
102 return previousDebugInfo;
103 }
104
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 +
124 let didWarnAboutMaps;
125 let didWarnAboutGenerators;
126 let ownerHasKeyUseWarning;
@@ -274,7 +294,7 @@ function coerceRef(workInProgress: Fiber, element: ReactElement): void {
294 workInProgress.ref = refProp !== undefined ? refProp : null;
295 }
296
277 -function throwOnInvalidObjectType(returnFiber: Fiber, newChild: Object) {
297 +function throwOnInvalidObjectTypeImpl(returnFiber: Fiber, newChild: Object) {
298 if (newChild.$$typeof === REACT_LEGACY_ELEMENT_TYPE) {
299 throw new Error(
300 'A React Element from an older version of React was rendered. ' +
@@ -299,7 +319,18 @@ function throwOnInvalidObjectType(returnFiber: Fiber, newChild: Object) {
319 );
320 }
321
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) {
334 if (__DEV__) {
335 const parentName = getComponentNameFromFiber(returnFiber) || 'Component';
336
@@ -336,7 +367,16 @@ function warnOnFunctionType(returnFiber: Fiber, invalidChild: Function) {
367 }
368 }
369
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) {
380 if (__DEV__) {
381 const parentName = getComponentNameFromFiber(returnFiber) || 'Component';
382
@@ -364,6 +404,15 @@ function warnOnSymbolType(returnFiber: Fiber, invalidChild: symbol) {
404 }
405 }
406
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 +
416 type ChildReconciler = (
417 returnFiber: Fiber,
418 currentFirstChild: Fiber | null,
@@ -1941,12 +1990,14 @@ function createChildReconciler(
1990 throwFiber.return = returnFiber;
1991 if (__DEV__) {
1992 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.
2001 throwFiber._debugOwner = returnFiber._debugOwner;
2002 throwFiber._debugTask = returnFiber._debugTask;
2003 if (debugInfo != null) {
packages/react-server/src/ReactFlightServer.js
+33 -14
@@ -864,7 +864,7 @@ function serializeDebugThenable(
864 const x = thenable.reason;
865 // We don't log these errors since they didn't actually throw into Flight.
866 const digest = '';
867 - emitErrorChunk(request, id, digest, x, true);
867 + emitErrorChunk(request, id, digest, x, true, null);
868 return ref;
869 }
870 }
@@ -916,7 +916,7 @@ function serializeDebugThenable(
916 }
917 // We don't log these errors since they didn't actually throw into Flight.
918 const digest = '';
919 - emitErrorChunk(request, id, digest, reason, true);
919 + emitErrorChunk(request, id, digest, reason, true, null);
920 enqueueFlush(request);
921 },
922 );
@@ -964,7 +964,7 @@ function emitRequestedDebugThenable(
964 }
965 // We don't log these errors since they didn't actually throw into Flight.
966 const digest = '';
967 - emitErrorChunk(request, id, digest, reason, true);
967 + emitErrorChunk(request, id, digest, reason, true, null);
968 enqueueFlush(request);
969 },
970 );
@@ -2764,7 +2764,7 @@ function serializeClientReference(
2764 request.pendingChunks++;
2765 const errorId = request.nextChunkId++;
2766 const digest = logRecoverableError(request, x, null);
2767 - emitErrorChunk(request, errorId, digest, x, false);
2767 + emitErrorChunk(request, errorId, digest, x, false, null);
2768 return serializeByValueID(errorId);
2769 }
2770 }
@@ -2813,7 +2813,7 @@ function serializeDebugClientReference(
2813 request.pendingDebugChunks++;
2814 const errorId = request.nextChunkId++;
2815 const digest = logRecoverableError(request, x, null);
2816 - emitErrorChunk(request, errorId, digest, x, true);
2816 + emitErrorChunk(request, errorId, digest, x, true, null);
2817 return serializeByValueID(errorId);
2818 }
2819 }
@@ -3054,7 +3054,7 @@ function serializeDebugBlob(request: Request, blob: Blob): string {
3054 }
3055 function error(reason: mixed) {
3056 const digest = '';
3057 - emitErrorChunk(request, id, digest, reason, true);
3057 + emitErrorChunk(request, id, digest, reason, true, null);
3058 enqueueFlush(request);
3059 // $FlowFixMe should be able to pass mixed
3060 reader.cancel(reason).then(noop, noop);
@@ -3254,7 +3254,14 @@ function renderModel(
3254 emitPostponeChunk(request, errorId, postponeInstance);
3255 } else {
3256 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 + );
3265 }
3266 if (wasReactNode) {
3267 // We'll replace this element with a lazy reference that throws on the client
@@ -4072,7 +4079,8 @@ function emitErrorChunk(
4079 id: number,
4080 digest: string,
4081 error: mixed,
4075 - debug: boolean,
4082 + debug: boolean, // DEV-only
4083 + owner: ?ReactComponentInfo, // DEV-only
4084 ): void {
4085 let errorInfo: ReactErrorInfo;
4086 if (__DEV__) {
@@ -4104,7 +4112,9 @@ function emitErrorChunk(
4112 message = 'An error occurred but serializing the error message failed.';
4113 stack = [];
4114 }
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};
4118 } else {
4119 errorInfo = {digest};
4120 }
@@ -4204,7 +4214,7 @@ function emitDebugChunk(
4214 function outlineComponentInfo(
4215 request: Request,
4216 componentInfo: ReactComponentInfo,
4207 -): void {
4217 +): string {
4218 if (!__DEV__) {
4219 // These errors should never make it into a build so we don't need to encode them in codes.json
4220 // eslint-disable-next-line react-internal/prod-error-codes
@@ -4213,9 +4223,10 @@ function outlineComponentInfo(
4223 );
4224 }
4225
4216 - if (request.writtenDebugObjects.has(componentInfo)) {
4226 + const existingRef = request.writtenDebugObjects.get(componentInfo);
4227 + if (existingRef !== undefined) {
4228 // Already written
4218 - return;
4229 + return existingRef;
4230 }
4231
4232 if (componentInfo.owner != null) {
@@ -4270,6 +4281,7 @@ function outlineComponentInfo(
4281 request.writtenDebugObjects.set(componentInfo, ref);
4282 // We also store this in the main dedupe set so that it can be referenced by inline React Elements.
4283 request.writtenObjects.set(componentInfo, ref);
4284 + return ref;
4285 }
4286
4287 function emitIOInfoChunk(
@@ -5465,7 +5477,14 @@ function erroredTask(request: Request, task: Task, error: mixed): void {
5477 emitPostponeChunk(request, task.id, postponeInstance);
5478 } else {
5479 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 + );
5488 }
5489 request.abortableTasks.delete(task);
5490 callOnAllReadyIfReady(request);
@@ -6040,7 +6059,7 @@ export function abort(request: Request, reason: mixed): void {
6059 const errorId = request.nextChunkId++;
6060 request.fatalError = errorId;
6061 request.pendingChunks++;
6043 - emitErrorChunk(request, errorId, digest, error, false);
6062 + emitErrorChunk(request, errorId, digest, error, false, null);
6063 abortableTasks.forEach(task => abortTask(task, request, errorId));
6064 scheduleWork(() => finishAbort(request, abortableTasks, errorId));
6065 }
packages/shared/ReactTypes.js
+1
@@ -228,6 +228,7 @@ export type ReactErrorInfoDev = {
228 +message: string,
229 +stack: ReactStackTrace,
230 +env: string,
231 + +owner?: null | string,
232 };
233
234 export type ReactErrorInfo = ReactErrorInfoProd | ReactErrorInfoDev;