diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 6cb21229c3..74a412d6f4 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -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; diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js index 3bde0d6db9..f0d9c2e012 100644 --- a/packages/react-reconciler/src/ReactChildFiber.js +++ b/packages/react-reconciler/src/ReactChildFiber.js @@ -13,6 +13,7 @@ import type { Thenable, ReactContext, ReactDebugInfo, + ReactComponentInfo, SuspenseListRevealOrder, } from 'shared/ReactTypes'; import type {Fiber} from './ReactInternalTypes'; @@ -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; @@ -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. ' + @@ -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'; @@ -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'; @@ -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)); + } else { + warnOnSymbolTypeImpl(returnFiber, invalidChild); + } +} + type ChildReconciler = ( returnFiber: Fiber, currentFirstChild: Fiber | null, @@ -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) { diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 10edd1e5c3..31bea759a0 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -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; } } @@ -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); }, ); @@ -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); }, ); @@ -2764,7 +2764,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); } } @@ -2813,7 +2813,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); } } @@ -3054,7 +3054,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); @@ -3254,7 +3254,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 @@ -4072,7 +4079,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__) { @@ -4104,7 +4112,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}; } @@ -4204,7 +4214,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 @@ -4213,9 +4223,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) { @@ -4270,6 +4281,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( @@ -5465,7 +5477,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); @@ -6040,7 +6059,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)); } diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index f222823325..0b8d222e5c 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -228,6 +228,7 @@ export type ReactErrorInfoDev = { +message: string, +stack: ReactStackTrace, +env: string, + +owner?: null | string, }; export type ReactErrorInfo = ReactErrorInfoProd | ReactErrorInfoDev;