Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -3181,11 +3181,27 @@ function resolveErrorDev(
'An error occurred in the Server Components render but no message was provided',
),
);
const rootTask = getRootTask(response, env);
if (rootTask != null) {
error = rootTask.run(callStack);

let ownerTask: null | ConsoleTask = null;
if (errorInfo.owner != null) {
const ownerRef = errorInfo.owner.slice(1);
// TODO: This is not resilient to the owner loading later in an Error like a debug channel.
// The whole error serialization should probably go through the regular model at least for DEV.
const owner = getOutlinedModel(response, ownerRef, {}, '', createModel);
if (owner !== null) {
ownerTask = initializeFakeTask(response, owner);
}
}

if (ownerTask === null) {
const rootTask = getRootTask(response, env);
if (rootTask != null) {
error = rootTask.run(callStack);
} else {
error = callStack();
}
} else {
error = callStack();
error = ownerTask.run(callStack);
}

(error: any).name = name;
Expand Down
69 changes: 60 additions & 9 deletions packages/react-reconciler/src/ReactChildFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
Thenable,
ReactContext,
ReactDebugInfo,
ReactComponentInfo,
SuspenseListRevealOrder,
} from 'shared/ReactTypes';
import type {Fiber} from './ReactInternalTypes';
Expand Down Expand Up @@ -101,6 +102,25 @@ function pushDebugInfo(
return previousDebugInfo;
}

function getCurrentDebugTask(): null | ConsoleTask {
// Get the debug task of the parent Server Component if there is one.
if (__DEV__) {
const debugInfo = currentDebugInfo;
if (debugInfo != null) {
for (let i = debugInfo.length - 1; i >= 0; i--) {
if (debugInfo[i].name != null) {
const componentInfo: ReactComponentInfo = debugInfo[i];
const debugTask: ?ConsoleTask = componentInfo.debugTask;
if (debugTask != null) {
return debugTask;
}
}
}
}
}
return null;
}

let didWarnAboutMaps;
let didWarnAboutGenerators;
let ownerHasKeyUseWarning;
Expand Down Expand Up @@ -274,7 +294,7 @@ function coerceRef(workInProgress: Fiber, element: ReactElement): void {
workInProgress.ref = refProp !== undefined ? refProp : null;
}

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

function warnOnFunctionType(returnFiber: Fiber, invalidChild: Function) {
function throwOnInvalidObjectType(returnFiber: Fiber, newChild: Object) {
const debugTask = getCurrentDebugTask();
if (__DEV__ && debugTask !== null) {
debugTask.run(
throwOnInvalidObjectTypeImpl.bind(null, returnFiber, newChild),
);
} else {
throwOnInvalidObjectTypeImpl(returnFiber, newChild);
}
}

function warnOnFunctionTypeImpl(returnFiber: Fiber, invalidChild: Function) {
if (__DEV__) {
const parentName = getComponentNameFromFiber(returnFiber) || 'Component';

Expand Down Expand Up @@ -336,7 +367,16 @@ function warnOnFunctionType(returnFiber: Fiber, invalidChild: Function) {
}
}

function warnOnSymbolType(returnFiber: Fiber, invalidChild: symbol) {
function warnOnFunctionType(returnFiber: Fiber, invalidChild: Function) {
const debugTask = getCurrentDebugTask();
if (__DEV__ && debugTask !== null) {
debugTask.run(warnOnFunctionTypeImpl.bind(null, returnFiber, invalidChild));
} else {
warnOnFunctionTypeImpl(returnFiber, invalidChild);
}
}

function warnOnSymbolTypeImpl(returnFiber: Fiber, invalidChild: symbol) {
if (__DEV__) {
const parentName = getComponentNameFromFiber(returnFiber) || 'Component';

Expand Down Expand Up @@ -364,6 +404,15 @@ function warnOnSymbolType(returnFiber: Fiber, invalidChild: symbol) {
}
}

function warnOnSymbolType(returnFiber: Fiber, invalidChild: symbol) {
const debugTask = getCurrentDebugTask();
if (__DEV__ && debugTask !== null) {
debugTask.run(warnOnSymbolTypeImpl.bind(null, returnFiber, invalidChild));
Copy link
Collaborator Author

@sebmarkbage sebmarkbage Sep 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically these should probably use this technique instead of creating a fake Fiber that gets logged:

https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactChildFiber.js#L224-L234

So that captureOwnerStack inside console.error can also observe it.

But I'm not bothering with that for now.

It doesn't matter for the throws since Error constructor doesn't observe it anyway.

} else {
warnOnSymbolTypeImpl(returnFiber, invalidChild);
}
}

type ChildReconciler = (
returnFiber: Fiber,
currentFirstChild: Fiber | null,
Expand Down Expand Up @@ -1941,12 +1990,14 @@ function createChildReconciler(
throwFiber.return = returnFiber;
if (__DEV__) {
const debugInfo = (throwFiber._debugInfo = currentDebugInfo);
// 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 them 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.
// Conceptually the error's owner should ideally be captured when the
// Error constructor is called but we don't override them 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.
// In newer Chrome, Error constructor does capture the Task which is what
// is logged by reportError. In that case this debugTask isn't used.
throwFiber._debugOwner = returnFiber._debugOwner;
throwFiber._debugTask = returnFiber._debugTask;
if (debugInfo != null) {
Expand Down
47 changes: 33 additions & 14 deletions packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -864,7 +864,7 @@ function serializeDebugThenable(
const x = thenable.reason;
// We don't log these errors since they didn't actually throw into Flight.
const digest = '';
emitErrorChunk(request, id, digest, x, true);
emitErrorChunk(request, id, digest, x, true, null);
return ref;
}
}
Expand Down Expand Up @@ -916,7 +916,7 @@ function serializeDebugThenable(
}
// We don't log these errors since they didn't actually throw into Flight.
const digest = '';
emitErrorChunk(request, id, digest, reason, true);
emitErrorChunk(request, id, digest, reason, true, null);
enqueueFlush(request);
},
);
Expand Down Expand Up @@ -964,7 +964,7 @@ function emitRequestedDebugThenable(
}
// We don't log these errors since they didn't actually throw into Flight.
const digest = '';
emitErrorChunk(request, id, digest, reason, true);
emitErrorChunk(request, id, digest, reason, true, null);
enqueueFlush(request);
},
);
Expand Down Expand Up @@ -2759,7 +2759,7 @@ function serializeClientReference(
request.pendingChunks++;
const errorId = request.nextChunkId++;
const digest = logRecoverableError(request, x, null);
emitErrorChunk(request, errorId, digest, x, false);
emitErrorChunk(request, errorId, digest, x, false, null);
return serializeByValueID(errorId);
}
}
Expand Down Expand Up @@ -2808,7 +2808,7 @@ function serializeDebugClientReference(
request.pendingDebugChunks++;
const errorId = request.nextChunkId++;
const digest = logRecoverableError(request, x, null);
emitErrorChunk(request, errorId, digest, x, true);
emitErrorChunk(request, errorId, digest, x, true, null);
return serializeByValueID(errorId);
}
}
Expand Down Expand Up @@ -3049,7 +3049,7 @@ function serializeDebugBlob(request: Request, blob: Blob): string {
}
function error(reason: mixed) {
const digest = '';
emitErrorChunk(request, id, digest, reason, true);
emitErrorChunk(request, id, digest, reason, true, null);
enqueueFlush(request);
// $FlowFixMe should be able to pass mixed
reader.cancel(reason).then(noop, noop);
Expand Down Expand Up @@ -3249,7 +3249,14 @@ function renderModel(
emitPostponeChunk(request, errorId, postponeInstance);
} else {
const digest = logRecoverableError(request, x, task);
emitErrorChunk(request, errorId, digest, x, false);
emitErrorChunk(
request,
errorId,
digest,
x,
false,
__DEV__ ? task.debugOwner : null,
);
}
if (wasReactNode) {
// We'll replace this element with a lazy reference that throws on the client
Expand Down Expand Up @@ -4067,7 +4074,8 @@ function emitErrorChunk(
id: number,
digest: string,
error: mixed,
debug: boolean,
debug: boolean, // DEV-only
owner: ?ReactComponentInfo, // DEV-only
): void {
let errorInfo: ReactErrorInfo;
if (__DEV__) {
Expand Down Expand Up @@ -4099,7 +4107,9 @@ function emitErrorChunk(
message = 'An error occurred but serializing the error message failed.';
stack = [];
}
errorInfo = {digest, name, message, stack, env};
const ownerRef =
owner == null ? null : outlineComponentInfo(request, owner);
errorInfo = {digest, name, message, stack, env, owner: ownerRef};
} else {
errorInfo = {digest};
}
Expand Down Expand Up @@ -4199,7 +4209,7 @@ function emitDebugChunk(
function outlineComponentInfo(
request: Request,
componentInfo: ReactComponentInfo,
): void {
): string {
if (!__DEV__) {
// These errors should never make it into a build so we don't need to encode them in codes.json
// eslint-disable-next-line react-internal/prod-error-codes
Expand All @@ -4208,9 +4218,10 @@ function outlineComponentInfo(
);
}

if (request.writtenDebugObjects.has(componentInfo)) {
const existingRef = request.writtenDebugObjects.get(componentInfo);
if (existingRef !== undefined) {
// Already written
return;
return existingRef;
}

if (componentInfo.owner != null) {
Expand Down Expand Up @@ -4265,6 +4276,7 @@ function outlineComponentInfo(
request.writtenDebugObjects.set(componentInfo, ref);
// We also store this in the main dedupe set so that it can be referenced by inline React Elements.
request.writtenObjects.set(componentInfo, ref);
return ref;
}

function emitIOInfoChunk(
Expand Down Expand Up @@ -5456,7 +5468,14 @@ function erroredTask(request: Request, task: Task, error: mixed): void {
emitPostponeChunk(request, task.id, postponeInstance);
} else {
const digest = logRecoverableError(request, error, task);
emitErrorChunk(request, task.id, digest, error, false);
emitErrorChunk(
request,
task.id,
digest,
error,
false,
__DEV__ ? task.debugOwner : null,
);
}
request.abortableTasks.delete(task);
callOnAllReadyIfReady(request);
Expand Down Expand Up @@ -6031,7 +6050,7 @@ export function abort(request: Request, reason: mixed): void {
const errorId = request.nextChunkId++;
request.fatalError = errorId;
request.pendingChunks++;
emitErrorChunk(request, errorId, digest, error, false);
emitErrorChunk(request, errorId, digest, error, false, null);
abortableTasks.forEach(task => abortTask(task, request, errorId));
scheduleWork(() => finishAbort(request, abortableTasks, errorId));
}
Expand Down
1 change: 1 addition & 0 deletions packages/shared/ReactTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ export type ReactErrorInfoDev = {
+message: string,
+stack: ReactStackTrace,
+env: string,
+owner?: null | string,
};

export type ReactErrorInfo = ReactErrorInfoProd | ReactErrorInfoDev;
Expand Down
Loading