diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 874183bb4d541..eb739eb00ba59 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -734,7 +734,7 @@ describe('ReactDOMFizzServer', () => { const theError = new Error('Test'); const loggedErrors = []; - function onError(x) { + function onError(x, errorInfo) { loggedErrors.push(x); return 'Hash of (' + x.message + ')'; } @@ -837,7 +837,7 @@ describe('ReactDOMFizzServer', () => { const theError = new Error('Test'); const loggedErrors = []; - function onError(x) { + function onError(x, errorInfo) { loggedErrors.push(x); return 'hash of (' + x.message + ')'; } @@ -898,7 +898,7 @@ describe('ReactDOMFizzServer', () => { [ theError.message, expectedDigest, - componentStack(['Suspense', 'div', 'App']), + componentStack(['Lazy', 'Suspense', 'div', 'App']), ], ], [ @@ -936,7 +936,9 @@ describe('ReactDOMFizzServer', () => { return (
loading...}> - + + +
); @@ -979,7 +981,15 @@ describe('ReactDOMFizzServer', () => { [ theError.message, expectedDigest, - componentStack(['Erroring', 'Suspense', 'div', 'App']), + componentStack([ + 'Erroring', + 'Indirection', + 'Indirection', + 'Indirection', + 'Suspense', + 'div', + 'App', + ]), ], ], [ @@ -1330,6 +1340,11 @@ describe('ReactDOMFizzServer', () => { +
+ + + +
); } @@ -1359,7 +1374,11 @@ describe('ReactDOMFizzServer', () => { await waitForAll([]); // We're still loading because we're waiting for the server to stream more content. - expect(getVisibleChildren(container)).toEqual(
Loading...
); + expect(getVisibleChildren(container)).toEqual( +
+ Loading...
loading...
+
, + ); // We abort the server response. await act(() => { @@ -1374,26 +1393,44 @@ describe('ReactDOMFizzServer', () => { [ 'The server did not finish this Suspense boundary: The render was aborted by the server without a reason.', expectedDigest, + // We get the stack of the task when it was aborted which is why we see `h1` componentStack(['h1', 'Suspense', 'div', 'App']), ], + [ + 'The server did not finish this Suspense boundary: The render was aborted by the server without a reason.', + expectedDigest, + componentStack(['Suspense', 'main', 'div', 'App']), + ], ], [ [ 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.', expectedDigest, ], + [ + 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.', + expectedDigest, + ], ], ); - expect(getVisibleChildren(container)).toEqual(
Loading...
); + expect(getVisibleChildren(container)).toEqual( +
+ Loading...
loading...
+
, + ); // We now resolve it on the client. - await clientAct(() => resolveText('Hello')); + await clientAct(() => { + resolveText('Hello'); + resolveText('World'); + }); assertLog([]); // The client rendered HTML is now in place. expect(getVisibleChildren(container)).toEqual(

Hello

+
World
, ); }); diff --git a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js index e1dc396782265..14c4a597922ee 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js @@ -7,7 +7,11 @@ * @flow */ -import type {PostponedState} from 'react-server/src/ReactFizzServer'; +import type { + PostponedState, + ErrorInfo, + PostponeInfo, +} from 'react-server/src/ReactFizzServer'; import type {ReactNodeList, ReactFormState} from 'shared/ReactTypes'; import type { BootstrapScriptDescriptor, @@ -42,8 +46,8 @@ type Options = { bootstrapModules?: Array, progressiveChunkSize?: number, signal?: AbortSignal, - onError?: (error: mixed) => ?string, - onPostpone?: (reason: string) => void, + onError?: (error: mixed, errorInfo: ErrorInfo) => ?string, + onPostpone?: (reason: string, postponeInfo: PostponeInfo) => void, unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, importMap?: ImportMap, formState?: ReactFormState | null, diff --git a/packages/react-dom/src/server/ReactDOMFizzServerBun.js b/packages/react-dom/src/server/ReactDOMFizzServerBun.js index 309eb2b8d9d5b..ffe29f22ed7de 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerBun.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerBun.js @@ -13,6 +13,7 @@ import type { HeadersDescriptor, } from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; import type {ImportMap} from '../shared/ReactDOMTypes'; +import type {ErrorInfo, PostponeInfo} from 'react-server/src/ReactFizzServer'; import ReactVersion from 'shared/ReactVersion'; @@ -39,8 +40,8 @@ type Options = { bootstrapModules?: Array, progressiveChunkSize?: number, signal?: AbortSignal, - onError?: (error: mixed) => ?string, - onPostpone?: (reason: string) => void, + onError?: (error: mixed, errorInfo: ErrorInfo) => ?string, + onPostpone?: (reason: string, postponeInfo: PostponeInfo) => void, unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, importMap?: ImportMap, formState?: ReactFormState | null, diff --git a/packages/react-dom/src/server/ReactDOMFizzServerEdge.js b/packages/react-dom/src/server/ReactDOMFizzServerEdge.js index e1dc396782265..14c4a597922ee 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerEdge.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerEdge.js @@ -7,7 +7,11 @@ * @flow */ -import type {PostponedState} from 'react-server/src/ReactFizzServer'; +import type { + PostponedState, + ErrorInfo, + PostponeInfo, +} from 'react-server/src/ReactFizzServer'; import type {ReactNodeList, ReactFormState} from 'shared/ReactTypes'; import type { BootstrapScriptDescriptor, @@ -42,8 +46,8 @@ type Options = { bootstrapModules?: Array, progressiveChunkSize?: number, signal?: AbortSignal, - onError?: (error: mixed) => ?string, - onPostpone?: (reason: string) => void, + onError?: (error: mixed, errorInfo: ErrorInfo) => ?string, + onPostpone?: (reason: string, postponeInfo: PostponeInfo) => void, unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, importMap?: ImportMap, formState?: ReactFormState | null, diff --git a/packages/react-dom/src/server/ReactDOMFizzServerNode.js b/packages/react-dom/src/server/ReactDOMFizzServerNode.js index a37bea02c8aea..049849a664559 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerNode.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerNode.js @@ -7,7 +7,12 @@ * @flow */ -import type {Request, PostponedState} from 'react-server/src/ReactFizzServer'; +import type { + Request, + PostponedState, + ErrorInfo, + PostponeInfo, +} from 'react-server/src/ReactFizzServer'; import type {ReactNodeList, ReactFormState} from 'shared/ReactTypes'; import type {Writable} from 'stream'; import type { @@ -59,8 +64,8 @@ type Options = { onShellReady?: () => void, onShellError?: (error: mixed) => void, onAllReady?: () => void, - onError?: (error: mixed) => ?string, - onPostpone?: (reason: string) => void, + onError?: (error: mixed, errorInfo: ErrorInfo) => ?string, + onPostpone?: (reason: string, postponeInfo: PostponeInfo) => void, unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, importMap?: ImportMap, formState?: ReactFormState | null, @@ -73,8 +78,8 @@ type ResumeOptions = { onShellReady?: () => void, onShellError?: (error: mixed) => void, onAllReady?: () => void, - onError?: (error: mixed) => ?string, - onPostpone?: (reason: string) => void, + onError?: (error: mixed, errorInfo: ErrorInfo) => ?string, + onPostpone?: (reason: string, postponeInfo: PostponeInfo) => void, }; type PipeableStream = { diff --git a/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js b/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js index 7b5200d6183d7..cbc5cd4044361 100644 --- a/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js +++ b/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js @@ -12,7 +12,11 @@ import type { BootstrapScriptDescriptor, HeadersDescriptor, } from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; -import type {PostponedState} from 'react-server/src/ReactFizzServer'; +import type { + PostponedState, + ErrorInfo, + PostponeInfo, +} from 'react-server/src/ReactFizzServer'; import type {ImportMap} from '../shared/ReactDOMTypes'; import ReactVersion from 'shared/ReactVersion'; @@ -40,8 +44,8 @@ type Options = { bootstrapModules?: Array, progressiveChunkSize?: number, signal?: AbortSignal, - onError?: (error: mixed) => ?string, - onPostpone?: (reason: string) => void, + onError?: (error: mixed, errorInfo: ErrorInfo) => ?string, + onPostpone?: (reason: string, postponeInfo: PostponeInfo) => void, unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, importMap?: ImportMap, onHeaders?: (headers: Headers) => void, diff --git a/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js b/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js index 3cac30b1d9ac0..e1fc514b7da5b 100644 --- a/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js +++ b/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js @@ -12,7 +12,11 @@ import type { BootstrapScriptDescriptor, HeadersDescriptor, } from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; -import type {PostponedState} from 'react-server/src/ReactFizzServer'; +import type { + PostponedState, + ErrorInfo, + PostponeInfo, +} from 'react-server/src/ReactFizzServer'; import type {ImportMap} from '../shared/ReactDOMTypes'; import ReactVersion from 'shared/ReactVersion'; @@ -40,8 +44,8 @@ type Options = { bootstrapModules?: Array, progressiveChunkSize?: number, signal?: AbortSignal, - onError?: (error: mixed) => ?string, - onPostpone?: (reason: string) => void, + onError?: (error: mixed, errorInfo: ErrorInfo) => ?string, + onPostpone?: (reason: string, postponeInfo: PostponeInfo) => void, unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, importMap?: ImportMap, onHeaders?: (headers: Headers) => void, diff --git a/packages/react-dom/src/server/ReactDOMFizzStaticNode.js b/packages/react-dom/src/server/ReactDOMFizzStaticNode.js index 938749cb6afe0..3c3c4116a5e54 100644 --- a/packages/react-dom/src/server/ReactDOMFizzStaticNode.js +++ b/packages/react-dom/src/server/ReactDOMFizzStaticNode.js @@ -12,7 +12,11 @@ import type { BootstrapScriptDescriptor, HeadersDescriptor, } from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; -import type {PostponedState} from 'react-server/src/ReactFizzServer'; +import type { + PostponedState, + ErrorInfo, + PostponeInfo, +} from 'react-server/src/ReactFizzServer'; import type {ImportMap} from '../shared/ReactDOMTypes'; import {Writable, Readable} from 'stream'; @@ -41,8 +45,8 @@ type Options = { bootstrapModules?: Array, progressiveChunkSize?: number, signal?: AbortSignal, - onError?: (error: mixed) => ?string, - onPostpone?: (reason: string) => void, + onError?: (error: mixed, errorInfo: ErrorInfo) => ?string, + onPostpone?: (reason: string, postponeInfo: PostponeInfo) => void, unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, importMap?: ImportMap, onHeaders?: (headers: HeadersDescriptor) => void, diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 8909fccc691a6..cd34e285b1e74 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -229,7 +229,7 @@ type RenderTask = { legacyContext: LegacyContext, // the current legacy context that this task is executing in context: ContextSnapshot, // the current new context that this task is executing in treeContext: TreeContext, // the current tree context that this task is executing in - componentStack: null | ComponentStackNode, // DEV-only component stack + componentStack: null | ComponentStackNode, // stack frame description of the currently rendering component thenableState: null | ThenableState, }; @@ -254,7 +254,7 @@ type ReplayTask = { legacyContext: LegacyContext, // the current legacy context that this task is executing in context: ContextSnapshot, // the current new context that this task is executing in treeContext: TreeContext, // the current tree context that this task is executing in - componentStack: null | ComponentStackNode, // DEV-only component stack + componentStack: null | ComponentStackNode, // stack frame description of the currently rendering component thenableState: null | ThenableState, }; @@ -312,7 +312,7 @@ export opaque type Request = { // onError is called when an error happens anywhere in the tree. It might recover. // The return string is used in production primarily to avoid leaking internals, secondarily to save bytes. // Returning null/undefined will cause a defualt error message in production - onError: (error: mixed) => ?string, + onError: (error: mixed, errorInfo: ThrownInfo) => ?string, // onAllReady is called when all pending task is done but it may not have flushed yet. // This is a good time to start writing if you want only HTML and no intermediate steps. onAllReady: () => void, @@ -326,7 +326,7 @@ export opaque type Request = { onFatalError: (error: mixed) => void, // onPostpone is called when postpone() is called anywhere in the tree, which will defer // rendering - e.g. to the client. This is considered intentional and not an error. - onPostpone: (reason: string) => void, + onPostpone: (reason: string, postponeInfo: ThrownInfo) => void, // Form state that was the result of an MPA submission, if it was provided. formState: null | ReactFormState, }; @@ -361,12 +361,12 @@ export function createRequest( renderState: RenderState, rootFormatContext: FormatContext, progressiveChunkSize: void | number, - onError: void | ((error: mixed) => ?string), + onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string), onAllReady: void | (() => void), onShellReady: void | (() => void), onShellError: void | ((error: mixed) => void), onFatalError: void | ((error: mixed) => void), - onPostpone: void | ((reason: string) => void), + onPostpone: void | ((reason: string, postponeInfo: PostponeInfo) => void), formState: void | null | ReactFormState, ): Request { prepareHostDispatcher(); @@ -427,6 +427,7 @@ export function createRequest( emptyContextObject, rootContextSnapshot, emptyTreeContext, + null, ); pingedTasks.push(rootTask); return request; @@ -438,12 +439,12 @@ export function createPrerenderRequest( renderState: RenderState, rootFormatContext: FormatContext, progressiveChunkSize: void | number, - onError: void | ((error: mixed) => ?string), + onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string), onAllReady: void | (() => void), onShellReady: void | (() => void), onShellError: void | ((error: mixed) => void), onFatalError: void | ((error: mixed) => void), - onPostpone: void | ((reason: string) => void), + onPostpone: void | ((reason: string, postponeInfo: PostponeInfo) => void), ): Request { const request = createRequest( children, @@ -472,12 +473,12 @@ export function resumeRequest( children: ReactNodeList, postponedState: PostponedState, renderState: RenderState, - onError: void | ((error: mixed) => ?string), + onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string), onAllReady: void | (() => void), onShellReady: void | (() => void), onShellError: void | ((error: mixed) => void), onFatalError: void | ((error: mixed) => void), - onPostpone: void | ((reason: string) => void), + onPostpone: void | ((reason: string, postponeInfo: PostponeInfo) => void), ): Request { prepareHostDispatcher(); const pingedTasks: Array = []; @@ -537,6 +538,7 @@ export function resumeRequest( emptyContextObject, rootContextSnapshot, emptyTreeContext, + null, ); pingedTasks.push(rootTask); return request; @@ -560,6 +562,7 @@ export function resumeRequest( emptyContextObject, rootContextSnapshot, emptyTreeContext, + null, ); pingedTasks.push(rootTask); return request; @@ -617,6 +620,7 @@ function createRenderTask( legacyContext: LegacyContext, context: ContextSnapshot, treeContext: TreeContext, + componentStack: null | ComponentStackNode, ): RenderTask { request.allPendingTasks++; if (blockedBoundary === null) { @@ -624,7 +628,7 @@ function createRenderTask( } else { blockedBoundary.pendingTasks++; } - const task: RenderTask = ({ + const task: RenderTask = { replay: null, node, childIndex, @@ -637,11 +641,9 @@ function createRenderTask( legacyContext, context, treeContext, + componentStack, thenableState, - }: any); - if (__DEV__) { - task.componentStack = null; - } + }; abortSet.add(task); return task; } @@ -659,6 +661,7 @@ function createReplayTask( legacyContext: LegacyContext, context: ContextSnapshot, treeContext: TreeContext, + componentStack: null | ComponentStackNode, ): ReplayTask { request.allPendingTasks++; if (blockedBoundary === null) { @@ -667,7 +670,7 @@ function createReplayTask( blockedBoundary.pendingTasks++; } replay.pendingTasks++; - const task: ReplayTask = ({ + const task: ReplayTask = { replay, node, childIndex, @@ -680,11 +683,9 @@ function createReplayTask( legacyContext, context, treeContext, + componentStack, thenableState, - }: any); - if (__DEV__) { - task.componentStack = null; - } + }; abortSet.add(task); return task; } @@ -723,53 +724,70 @@ function getCurrentStackInDEV(): string { return ''; } -function pushBuiltInComponentStackInDEV(task: Task, type: string): void { - if (__DEV__) { - task.componentStack = { - tag: 0, - parent: task.componentStack, - type, - }; - } +function getStackFromNode(stackNode: ComponentStackNode): string { + return getStackByComponentStackNode(stackNode); } -function pushFunctionComponentStackInDEV(task: Task, type: Function): void { - if (__DEV__) { - task.componentStack = { - tag: 1, - parent: task.componentStack, - type, - }; - } + +function createBuiltInComponentStack( + task: Task, + type: string, +): ComponentStackNode { + return { + tag: 0, + parent: task.componentStack, + type, + }; } -function pushClassComponentStackInDEV(task: Task, type: Function): void { - if (__DEV__) { - task.componentStack = { - tag: 2, - parent: task.componentStack, - type, - }; - } +function createFunctionComponentStack( + task: Task, + type: Function, +): ComponentStackNode { + return { + tag: 1, + parent: task.componentStack, + type, + }; } -function popComponentStackInDEV(task: Task): void { - if (__DEV__) { - if (task.componentStack === null) { - console.error( - 'Unexpectedly popped too many stack frames. This is a bug in React.', - ); - } else { - task.componentStack = task.componentStack.parent; - } - } +function createClassComponentStack( + task: Task, + type: Function, +): ComponentStackNode { + return { + tag: 2, + parent: task.componentStack, + type, + }; } -// stash the component stack of an unwinding error until it is processed -let lastBoundaryErrorComponentStackDev: ?string = null; +type ThrownInfo = { + componentStack?: string, +}; +export type ErrorInfo = ThrownInfo; +export type PostponeInfo = ThrownInfo; + +// While we track component stacks in prod all the time we only produce a reified stack in dev and +// during prerender in Prod. The reason for this is that the stack is useful for prerender where the timeliness +// of the request is less critical than the observability of the execution. For renders and resumes however we +// prioritize speed of the request. +function getThrownInfo(node: null | ComponentStackNode): ThrownInfo { + if (node) { + return { + componentStack: getStackFromNode(node), + }; + } else { + return {}; + } +} -function captureBoundaryErrorDetailsDev( +function encodeErrorForBoundary( boundary: SuspenseBoundary, + digest: ?string, error: mixed, + thrownInfo: ThrownInfo, ) { + boundary.errorDigest = digest; if (__DEV__) { + // In dev we additionally encode the error message and component stack on the boundary let errorMessage; if (typeof error === 'string') { errorMessage = error; @@ -780,30 +798,39 @@ function captureBoundaryErrorDetailsDev( errorMessage = String(error); } - const errorComponentStack = - lastBoundaryErrorComponentStackDev || getCurrentStackInDEV(); - lastBoundaryErrorComponentStackDev = null; - boundary.errorMessage = errorMessage; - boundary.errorComponentStack = errorComponentStack; + boundary.errorComponentStack = thrownInfo.componentStack; } } -function logPostpone(request: Request, reason: string): void { +function logPostpone( + request: Request, + reason: string, + postponeInfo: ThrownInfo, +): void { // If this callback errors, we intentionally let that error bubble up to become a fatal error // so that someone fixes the error reporting instead of hiding it. - request.onPostpone(reason); + request.onPostpone(reason, postponeInfo); } -function logRecoverableError(request: Request, error: any): ?string { +function logRecoverableError( + request: Request, + error: any, + errorInfo: ThrownInfo, +): ?string { // If this callback errors, we intentionally let that error bubble up to become a fatal error // so that someone fixes the error reporting instead of hiding it. - const errorDigest = request.onError(error); + const errorDigest = request.onError(error, errorInfo); if (errorDigest != null && typeof errorDigest !== 'string') { - // eslint-disable-next-line react-internal/prod-error-codes - throw new Error( - `onError returned something with a type other than "string". onError should return a string and may return null or undefined but must not return anything else. It received something of type "${typeof errorDigest}" instead`, - ); + // We used to throw here but since this gets called from a variety of unprotected places it + // seems better to just warn and discard the returned value. + if (__DEV__) { + console.error( + 'onError returned something with a type other than "string". onError should return a string and may return null or undefined but must not return anything else. It received something of type "%s" instead', + typeof errorDigest, + ); + } + return; } return errorDigest; } @@ -848,7 +875,11 @@ function renderSuspenseBoundary( // $FlowFixMe: Refined. const task: RenderTask = someTask; - pushBuiltInComponentStackInDEV(task, 'Suspense'); + const previousComponentStack = task.componentStack; + // If we end up creating the fallback task we need it to have the correct stack which is + // the stack for the boundary itself. We stash it here so we can use it if needed later + const suspenseComponentStack = (task.componentStack = + createBuiltInComponentStack(task, 'Suspense')); const prevKeyPath = task.keyPath; const parentBoundary = task.blockedBoundary; @@ -912,6 +943,7 @@ function renderSuspenseBoundary( ); } task.keyPath = keyPath; + try { // We use the safe form because we don't handle suspending here. Only error handling. renderNode(request, task, content, -1); @@ -924,16 +956,19 @@ function renderSuspenseBoundary( contentRootSegment.status = COMPLETED; queueCompletedSegment(newBoundary, contentRootSegment); if (newBoundary.pendingTasks === 0 && newBoundary.status === PENDING) { - newBoundary.status = COMPLETED; // This must have been the last segment we were waiting on. This boundary is now complete. // Therefore we won't need the fallback. We early return so that we don't have to create // the fallback. - popComponentStackInDEV(task); + newBoundary.status = COMPLETED; + + // We are returning early so we need to restore the + task.componentStack = previousComponentStack; return; } - } catch (error) { + } catch (error: mixed) { contentRootSegment.status = ERRORED; newBoundary.status = CLIENT_RENDERED; + const thrownInfo = getThrownInfo(task.componentStack); let errorDigest; if ( enablePostpone && @@ -942,16 +977,13 @@ function renderSuspenseBoundary( error.$$typeof === REACT_POSTPONE_TYPE ) { const postponeInstance: Postpone = (error: any); - logPostpone(request, postponeInstance.message); + logPostpone(request, postponeInstance.message, thrownInfo); // TODO: Figure out a better signal than a magic digest value. errorDigest = 'POSTPONE'; } else { - errorDigest = logRecoverableError(request, error); - } - newBoundary.errorDigest = errorDigest; - if (__DEV__) { - captureBoundaryErrorDetailsDev(newBoundary, error); + errorDigest = logRecoverableError(request, error, thrownInfo); } + encodeErrorForBoundary(newBoundary, errorDigest, error, thrownInfo); // We don't need to decrement any task numbers because we didn't spawn any new task. // We don't need to schedule any task because we know the parent has written yet. @@ -966,6 +998,7 @@ function renderSuspenseBoundary( task.blockedBoundary = parentBoundary; task.blockedSegment = parentSegment; task.keyPath = prevKeyPath; + task.componentStack = previousComponentStack; } const fallbackKeyPath = [keyPath[0], 'Suspense Fallback', keyPath[2]]; @@ -1005,15 +1038,13 @@ function renderSuspenseBoundary( task.legacyContext, task.context, task.treeContext, + // This stack should be the Suspense boundary stack because while the fallback is actually a child segment + // of the parent boundary from a component standpoint the fallback is a child of the Suspense boundary itself + suspenseComponentStack, ); - if (__DEV__) { - suspendedFallbackTask.componentStack = task.componentStack; - } // TODO: This should be queued at a separate lower priority queue so that we only work // on preparing fallbacks if we don't have any more main content to task on. request.pingedTasks.push(suspendedFallbackTask); - - popComponentStackInDEV(task); } function replaySuspenseBoundary( @@ -1027,7 +1058,11 @@ function replaySuspenseBoundary( fallbackNodes: Array, fallbackSlots: ResumeSlots, ): void { - pushBuiltInComponentStackInDEV(task, 'Suspense'); + const previousComponentStack = task.componentStack; + // If we end up creating the fallback task we need it to have the correct stack which is + // the stack for the boundary itself. We stash it here so we can use it if needed later + const suspenseComponentStack = (task.componentStack = + createBuiltInComponentStack(task, 'Suspense')); const prevKeyPath = task.keyPath; const previousReplaySet: ReplaySet = task.replay; @@ -1054,9 +1089,11 @@ function replaySuspenseBoundary( resumedBoundary.resources, ); } + try { // We use the safe form because we don't handle suspending here. Only error handling. renderNode(request, task, content, -1); + if (task.replay.pendingTasks === 1 && task.replay.nodes.length > 0) { throw new Error( "Couldn't find all resumable slots by key/index during replaying. " + @@ -1068,16 +1105,19 @@ function replaySuspenseBoundary( resumedBoundary.pendingTasks === 0 && resumedBoundary.status === PENDING ) { - resumedBoundary.status = COMPLETED; - request.completedBoundaries.push(resumedBoundary); // This must have been the last segment we were waiting on. This boundary is now complete. // Therefore we won't need the fallback. We early return so that we don't have to create // the fallback. - popComponentStackInDEV(task); + resumedBoundary.status = COMPLETED; + request.completedBoundaries.push(resumedBoundary); + // We restore the parent componentStack. Semantically this is the same as + // popComponentStack(task) but we do this instead because it should be slightly + // faster return; } - } catch (error) { + } catch (error: mixed) { resumedBoundary.status = CLIENT_RENDERED; + const thrownInfo = getThrownInfo(task.componentStack); let errorDigest; if ( enablePostpone && @@ -1086,16 +1126,13 @@ function replaySuspenseBoundary( error.$$typeof === REACT_POSTPONE_TYPE ) { const postponeInstance: Postpone = (error: any); - logPostpone(request, postponeInstance.message); + logPostpone(request, postponeInstance.message, thrownInfo); // TODO: Figure out a better signal than a magic digest value. errorDigest = 'POSTPONE'; } else { - errorDigest = logRecoverableError(request, error); - } - resumedBoundary.errorDigest = errorDigest; - if (__DEV__) { - captureBoundaryErrorDetailsDev(resumedBoundary, error); + errorDigest = logRecoverableError(request, error, thrownInfo); } + encodeErrorForBoundary(resumedBoundary, errorDigest, error, thrownInfo); task.replay.pendingTasks--; @@ -1115,6 +1152,7 @@ function replaySuspenseBoundary( task.blockedBoundary = parentBoundary; task.replay = previousReplaySet; task.keyPath = prevKeyPath; + task.componentStack = previousComponentStack; } const fallbackKeyPath = [keyPath[0], 'Suspense Fallback', keyPath[2]]; @@ -1139,15 +1177,13 @@ function replaySuspenseBoundary( task.legacyContext, task.context, task.treeContext, + // This stack should be the Suspense boundary stack because while the fallback is actually a child segment + // of the parent boundary from a component standpoint the fallback is a child of the Suspense boundary itself + suspenseComponentStack, ); - if (__DEV__) { - suspendedFallbackTask.componentStack = task.componentStack; - } // TODO: This should be queued at a separate lower priority queue so that we only work // on preparing fallbacks if we don't have any more main content to task on. request.pingedTasks.push(suspendedFallbackTask); - - popComponentStackInDEV(task); } function renderBackupSuspenseBoundary( @@ -1156,7 +1192,8 @@ function renderBackupSuspenseBoundary( keyPath: KeyNode, props: Object, ) { - pushBuiltInComponentStackInDEV(task, 'Suspense'); + const previousComponentStack = task.componentStack; + task.componentStack = createBuiltInComponentStack(task, 'Suspense'); const content = props.children; const segment = task.blockedSegment; @@ -1172,8 +1209,7 @@ function renderBackupSuspenseBoundary( pushEndCompletedSuspenseBoundary(segment.chunks); } task.keyPath = prevKeyPath; - - popComponentStackInDEV(task); + task.componentStack = previousComponentStack; } function renderHostElement( @@ -1183,7 +1219,8 @@ function renderHostElement( type: string, props: Object, ): void { - pushBuiltInComponentStackInDEV(task, type); + const previousComponentStack = task.componentStack; + task.componentStack = createBuiltInComponentStack(task, type); const segment = task.blockedSegment; if (segment === null) { // Replay @@ -1235,7 +1272,7 @@ function renderHostElement( ); segment.lastPushedText = false; } - popComponentStackInDEV(task); + task.componentStack = previousComponentStack; } function shouldConstruct(Component: any) { @@ -1316,14 +1353,15 @@ function renderClassComponent( Component: any, props: any, ): void { - pushClassComponentStackInDEV(task, Component); + const previousComponentStack = task.componentStack; + task.componentStack = createClassComponentStack(task, Component); const maskedContext = !disableLegacyContext ? getMaskedContext(Component, task.legacyContext) : undefined; const instance = constructClassInstance(Component, props, maskedContext); mountClassInstance(instance, Component, props, maskedContext); finishClassComponent(request, task, keyPath, instance, Component, props); - popComponentStackInDEV(task); + task.componentStack = previousComponentStack; } const didWarnAboutBadClass: {[string]: boolean} = {}; @@ -1350,7 +1388,8 @@ function renderIndeterminateComponent( if (!disableLegacyContext) { legacyContext = getMaskedContext(Component, task.legacyContext); } - pushFunctionComponentStackInDEV(task, Component); + const previousComponentStack = task.componentStack; + task.componentStack = createFunctionComponentStack(task, Component); if (__DEV__) { if ( @@ -1462,7 +1501,7 @@ function renderIndeterminateComponent( formStateMatchingIndex, ); } - popComponentStackInDEV(task); + task.componentStack = previousComponentStack; } function finishFunctionComponent( @@ -1601,7 +1640,8 @@ function renderForwardRef( props: Object, ref: any, ): void { - pushFunctionComponentStackInDEV(task, type.render); + const previousComponentStack = task.componentStack; + task.componentStack = createFunctionComponentStack(task, type.render); const children = renderWithHooks( request, task, @@ -1623,7 +1663,7 @@ function renderForwardRef( formStateCount, formStateMatchingIndex, ); - popComponentStackInDEV(task); + task.componentStack = previousComponentStack; } function renderMemo( @@ -1740,7 +1780,8 @@ function renderLazyComponent( props: Object, ref: any, ): void { - pushBuiltInComponentStackInDEV(task, 'Lazy'); + const previousComponentStack = task.componentStack; + task.componentStack = createBuiltInComponentStack(task, 'Lazy'); const payload = lazyComponent._payload; const init = lazyComponent._init; const Component = init(payload); @@ -1754,7 +1795,7 @@ function renderLazyComponent( resolvedProps, ref, ); - popComponentStackInDEV(task); + task.componentStack = previousComponentStack; } function renderOffscreen( @@ -1833,13 +1874,14 @@ function renderElement( return; } case REACT_SUSPENSE_LIST_TYPE: { - pushBuiltInComponentStackInDEV(task, 'SuspenseList'); + const preiousComponentStack = task.componentStack; + task.componentStack = createBuiltInComponentStack(task, 'SuspenseList'); // TODO: SuspenseList should control the boundaries. const prevKeyPath = task.keyPath; task.keyPath = keyPath; renderNodeDestructive(request, task, null, props.children, -1); task.keyPath = prevKeyPath; - popComponentStackInDEV(task); + task.componentStack = preiousComponentStack; return; } case REACT_SCOPE_TYPE: { @@ -2046,7 +2088,15 @@ function replayElement( // in the original prerender. What's unable to complete is the child // replay nodes which might be Suspense boundaries which are able to // absorb the error and we can still continue with siblings. - erroredReplay(request, task.blockedBoundary, x, childNodes, childSlots); + const thrownInfo = getThrownInfo(task.componentStack); + erroredReplay( + request, + task.blockedBoundary, + x, + thrownInfo, + childNodes, + childSlots, + ); } task.replay = replay; } else { @@ -2118,6 +2168,8 @@ function validateIterable(iterable, iteratorFn: Function): void { } } +// This function by it self renders a node and consumes the task by mutating it +// to update the current execution state. function renderNodeDestructive( request: Request, task: Task, @@ -2126,51 +2178,6 @@ function renderNodeDestructive( prevThenableState: ThenableState | null, node: ReactNodeList, childIndex: number, -): void { - if (__DEV__) { - // In Dev we wrap renderNodeDestructiveImpl in a try / catch so we can capture - // a component stack at the right place in the tree. We don't do this in renderNode - // becuase it is not called at every layer of the tree and we may lose frames - try { - return renderNodeDestructiveImpl( - request, - task, - prevThenableState, - node, - childIndex, - ); - } catch (x) { - if (typeof x === 'object' && x !== null && typeof x.then === 'function') { - // This is a Wakable, noop - } else { - // This is an error, stash the component stack if it is null. - lastBoundaryErrorComponentStackDev = - lastBoundaryErrorComponentStackDev !== null - ? lastBoundaryErrorComponentStackDev - : getCurrentStackInDEV(); - } - // rethrow so normal suspense logic can handle thrown value accordingly - throw x; - } - } else { - return renderNodeDestructiveImpl( - request, - task, - prevThenableState, - node, - childIndex, - ); - } -} - -// This function by it self renders a node and consumes the task by mutating it -// to update the current execution state. -function renderNodeDestructiveImpl( - request: Request, - task: Task, - prevThenableState: ThenableState | null, - node: ReactNodeList, - childIndex: number, ): void { if (task.replay !== null && typeof task.replay.slots === 'number') { // TODO: Figure out a cheaper place than this hot path to do this check. @@ -2232,30 +2239,18 @@ function renderNodeDestructiveImpl( 'Render them conditionally so that they only appear on the client render.', ); case REACT_LAZY_TYPE: { + const previousComponentStack = task.componentStack; + task.componentStack = createBuiltInComponentStack(task, 'Lazy'); const lazyNode: LazyComponentType = (node: any); const payload = lazyNode._payload; const init = lazyNode._init; - let resolvedNode; - if (__DEV__) { - try { - resolvedNode = init(payload); - } catch (x) { - if ( - typeof x === 'object' && - x !== null && - typeof x.then === 'function' - ) { - // this Lazy initializer is suspending. push a temporary frame onto the stack so it can be - // popped off in spawnNewSuspendedTask. This aligns stack behavior between Lazy in element position - // vs Component position. We do not want the frame for Errors so we exclusively do this in - // the wakeable branch - pushBuiltInComponentStackInDEV(task, 'Lazy'); - } - throw x; - } - } else { - resolvedNode = init(payload); - } + const resolvedNode = init(payload); + + // We restore the stack before rendering the resolved node because once the Lazy + // has resolved any future errors + task.componentStack = previousComponentStack; + + // Now we render the resolved node renderNodeDestructive(request, task, null, resolvedNode, childIndex); return; } @@ -2305,7 +2300,7 @@ function renderNodeDestructiveImpl( const maybeUsable: Object = node; if (typeof maybeUsable.then === 'function') { const thenable: Thenable = (maybeUsable: any); - return renderNodeDestructiveImpl( + return renderNodeDestructive( request, task, null, @@ -2319,7 +2314,7 @@ function renderNodeDestructiveImpl( maybeUsable.$$typeof === REACT_SERVER_CONTEXT_TYPE ) { const context: ReactContext = (maybeUsable: any); - return renderNodeDestructiveImpl( + return renderNodeDestructive( request, task, null, @@ -2429,7 +2424,15 @@ function replayFragment( // replay nodes which might be Suspense boundaries which are able to // absorb the error and we can still continue with siblings. // This is an error, stash the component stack if it is null. - erroredReplay(request, task.blockedBoundary, x, childNodes, childSlots); + const thrownInfo = getThrownInfo(task.componentStack); + erroredReplay( + request, + task.blockedBoundary, + x, + thrownInfo, + childNodes, + childSlots, + ); } task.replay = replay; // We finished rendering this node, so now we can consume this @@ -2664,8 +2667,9 @@ function injectPostponedHole( request: Request, task: RenderTask, reason: string, + thrownInfo: ThrownInfo, ): Segment { - logPostpone(request, reason); + logPostpone(request, reason, thrownInfo); // Something suspended, we'll need to create a new segment and resolve it later. const segment = task.blockedSegment; const insertionIndex = segment.chunks.length; @@ -2704,15 +2708,11 @@ function spawnNewSuspendedReplayTask( task.legacyContext, task.context, task.treeContext, + // We pop one task off the stack because the node that suspended will be tried again, + // which will add it back onto the stack. + task.componentStack !== null ? task.componentStack.parent : null, ); - if (__DEV__) { - if (task.componentStack !== null) { - // We pop one task off the stack because the node that suspended will be tried again, - // which will add it back onto the stack. - newTask.componentStack = task.componentStack.parent; - } - } const ping = newTask.ping; x.then(ping, ping); } @@ -2752,15 +2752,11 @@ function spawnNewSuspendedRenderTask( task.legacyContext, task.context, task.treeContext, + // We pop one task off the stack because the node that suspended will be tried again, + // which will add it back onto the stack. + task.componentStack !== null ? task.componentStack.parent : null, ); - if (__DEV__) { - if (task.componentStack !== null) { - // We pop one task off the stack because the node that suspended will be tried again, - // which will add it back onto the stack. - newTask.componentStack = task.componentStack.parent; - } - } const ping = newTask.ping; x.then(ping, ping); } @@ -2780,10 +2776,7 @@ function renderNode( const previousContext = task.context; const previousKeyPath = task.keyPath; const previousTreeContext = task.treeContext; - let previousComponentStack = null; - if (__DEV__) { - previousComponentStack = task.componentStack; - } + const previousComponentStack = task.componentStack; let x; // Store how much we've pushed at this point so we can reset it in case something // suspended partially through writing something. @@ -2825,11 +2818,9 @@ function renderNode( task.context = previousContext; task.keyPath = previousKeyPath; task.treeContext = previousTreeContext; + task.componentStack = previousComponentStack; // Restore all active ReactContexts to what they were before. switchContext(previousContext); - if (__DEV__) { - task.componentStack = previousComponentStack; - } return; } } @@ -2879,29 +2870,30 @@ function renderNode( task.context = previousContext; task.keyPath = previousKeyPath; task.treeContext = previousTreeContext; + task.componentStack = previousComponentStack; // Restore all active ReactContexts to what they were before. switchContext(previousContext); - if (__DEV__) { - task.componentStack = previousComponentStack; - } return; } if ( enablePostpone && - request.trackedPostpones !== null && x.$$typeof === REACT_POSTPONE_TYPE && + request.trackedPostpones !== null && task.blockedBoundary !== null // bubble if we're postponing in the shell ) { // If we're tracking postpones, we inject a hole here and continue rendering // sibling. Similar to suspending. If we're not tracking, we treat it more like // an error. Notably this doesn't spawn a new task since nothing will fill it // in during this prerender. - const postponeInstance: Postpone = (x: any); const trackedPostpones = request.trackedPostpones; + + const postponeInstance: Postpone = (x: any); + const thrownInfo = getThrownInfo(task.componentStack); const postponedSegment = injectPostponedHole( request, ((task: any): RenderTask), // We don't use ReplayTasks in prerenders. postponeInstance.message, + thrownInfo, ); trackPostpone(request, trackedPostpones, task, postponedSegment); @@ -2912,17 +2904,15 @@ function renderNode( task.context = previousContext; task.keyPath = previousKeyPath; task.treeContext = previousTreeContext; + task.componentStack = previousComponentStack; // Restore all active ReactContexts to what they were before. switchContext(previousContext); - if (__DEV__) { - task.componentStack = previousComponentStack; - } - lastBoundaryErrorComponentStackDev = null; return; } } } } + // Restore the context. We assume that this will be restored by the inner // functions in case nothing throws so we don't use "finally" here. task.formatContext = previousFormatContext; @@ -2930,13 +2920,13 @@ function renderNode( task.context = previousContext; task.keyPath = previousKeyPath; task.treeContext = previousTreeContext; + // We intentionally do not restore the component stack on the error pathway + // Whatever handles the error needs to use this stack which is the location of the + // error. We must restore the stack wherever we handle this + // Restore all active ReactContexts to what they were before. switchContext(previousContext); - if (__DEV__) { - task.componentStack = previousComponentStack; - } - // We assume that we don't need the correct context. - // Let's terminate the rest of the tree and don't render any siblings. + throw x; } @@ -2944,6 +2934,7 @@ function erroredReplay( request: Request, boundary: Root | SuspenseBoundary, error: mixed, + errorInfo: ThrownInfo, replayNodes: ReplayNode[], resumeSlots: ResumeSlots, ): void { @@ -2962,11 +2953,11 @@ function erroredReplay( error.$$typeof === REACT_POSTPONE_TYPE ) { const postponeInstance: Postpone = (error: any); - logPostpone(request, postponeInstance.message); + logPostpone(request, postponeInstance.message, errorInfo); // TODO: Figure out a better signal than a magic digest value. errorDigest = 'POSTPONE'; } else { - errorDigest = logRecoverableError(request, error); + errorDigest = logRecoverableError(request, error, errorInfo); } abortRemainingReplayNodes( request, @@ -2975,6 +2966,7 @@ function erroredReplay( resumeSlots, error, errorDigest, + errorInfo, ); } @@ -2982,6 +2974,7 @@ function erroredTask( request: Request, boundary: Root | SuspenseBoundary, error: mixed, + errorInfo: ThrownInfo, ) { // Report the error to a global handler. let errorDigest; @@ -2992,23 +2985,19 @@ function erroredTask( error.$$typeof === REACT_POSTPONE_TYPE ) { const postponeInstance: Postpone = (error: any); - logPostpone(request, postponeInstance.message); + logPostpone(request, postponeInstance.message, errorInfo); // TODO: Figure out a better signal than a magic digest value. errorDigest = 'POSTPONE'; } else { - errorDigest = logRecoverableError(request, error); + errorDigest = logRecoverableError(request, error, errorInfo); } if (boundary === null) { - lastBoundaryErrorComponentStackDev = null; fatalError(request, error); } else { boundary.pendingTasks--; if (boundary.status !== CLIENT_RENDERED) { boundary.status = CLIENT_RENDERED; - boundary.errorDigest = errorDigest; - if (__DEV__) { - captureBoundaryErrorDetailsDev(boundary, error); - } + encodeErrorForBoundary(boundary, errorDigest, error, errorInfo); // Regardless of what happens next, this boundary won't be displayed, // so we can flush it, if the parent already flushed. @@ -3019,8 +3008,6 @@ function erroredTask( // We reuse the same queue for errors. request.clientRenderedBoundaries.push(boundary); } - } else { - lastBoundaryErrorComponentStackDev = null; } } @@ -3048,6 +3035,7 @@ function abortRemainingSuspenseBoundary( rootSegmentID: number, error: mixed, errorDigest: ?string, + errorInfo: ThrownInfo, ): void { const resumedBoundary = createSuspenseBoundary(request, new Set()); resumedBoundary.parentFlushed = true; @@ -3055,24 +3043,18 @@ function abortRemainingSuspenseBoundary( resumedBoundary.rootSegmentID = rootSegmentID; resumedBoundary.status = CLIENT_RENDERED; - resumedBoundary.errorDigest = errorDigest; + let errorMessage = error; if (__DEV__) { const errorPrefix = 'The server did not finish this Suspense boundary: '; - let errorMessage; if (error && typeof error.message === 'string') { errorMessage = errorPrefix + error.message; } else { // eslint-disable-next-line react-internal/safe-string-coercion errorMessage = errorPrefix + String(error); } - const previousTaskInDev = currentTaskInDEV; - currentTaskInDEV = null; - try { - captureBoundaryErrorDetailsDev(resumedBoundary, errorMessage); - } finally { - currentTaskInDEV = previousTaskInDev; - } } + encodeErrorForBoundary(resumedBoundary, errorDigest, errorMessage, errorInfo); + if (resumedBoundary.parentFlushed) { request.clientRenderedBoundaries.push(resumedBoundary); } @@ -3085,6 +3067,7 @@ function abortRemainingReplayNodes( slots: ResumeSlots, error: mixed, errorDigest: ?string, + errorInfo: ThrownInfo, ): void { for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; @@ -3096,6 +3079,7 @@ function abortRemainingReplayNodes( node[3], error, errorDigest, + errorInfo, ); } else { const boundaryNode: ReplaySuspenseBoundary = node; @@ -3105,6 +3089,7 @@ function abortRemainingReplayNodes( rootSegmentID, error, errorDigest, + errorInfo, ); } } @@ -3121,10 +3106,7 @@ function abortRemainingReplayNodes( ); } else if (boundary.status !== CLIENT_RENDERED) { boundary.status = CLIENT_RENDERED; - boundary.errorDigest = errorDigest; - if (__DEV__) { - captureBoundaryErrorDetailsDev(boundary, error); - } + encodeErrorForBoundary(boundary, errorDigest, error, errorInfo); if (boundary.parentFlushed) { request.clientRenderedBoundaries.push(boundary); } @@ -3148,12 +3130,13 @@ function abortTask(task: Task, request: Request, error: mixed): void { } if (boundary === null) { + const errorInfo: ThrownInfo = {}; if (request.status !== CLOSING && request.status !== CLOSED) { const replay: null | ReplaySet = task.replay; if (replay === null) { // We didn't complete the root so we have nothing to show. We can close // the request; - logRecoverableError(request, error); + logRecoverableError(request, error, errorInfo); fatalError(request, error); return; } else { @@ -3162,7 +3145,7 @@ function abortTask(task: Task, request: Request, error: mixed): void { // the ReplaySet. replay.pendingTasks--; if (replay.pendingTasks === 0 && replay.nodes.length > 0) { - const errorDigest = logRecoverableError(request, error); + const errorDigest = logRecoverableError(request, error, errorInfo); abortRemainingReplayNodes( request, null, @@ -3170,6 +3153,7 @@ function abortTask(task: Task, request: Request, error: mixed): void { replay.slots, error, errorDigest, + errorInfo, ); } request.pendingRootTasks--; @@ -3182,25 +3166,23 @@ function abortTask(task: Task, request: Request, error: mixed): void { boundary.pendingTasks--; if (boundary.status !== CLIENT_RENDERED) { boundary.status = CLIENT_RENDERED; - boundary.errorDigest = logRecoverableError(request, error); + // We construct an errorInfo from the boundary's componentStack so the error in dev will indicate which + // boundary the message is referring to + const errorInfo = getThrownInfo(task.componentStack); + const errorDigest = logRecoverableError(request, error, errorInfo); + let errorMessage = error; if (__DEV__) { const errorPrefix = 'The server did not finish this Suspense boundary: '; - let errorMessage; if (error && typeof error.message === 'string') { errorMessage = errorPrefix + error.message; } else { // eslint-disable-next-line react-internal/safe-string-coercion errorMessage = errorPrefix + String(error); } - const previousTaskInDev = currentTaskInDEV; - currentTaskInDEV = task; - try { - captureBoundaryErrorDetailsDev(boundary, errorMessage); - } finally { - currentTaskInDEV = previousTaskInDev; - } } + encodeErrorForBoundary(boundary, errorDigest, errorMessage, errorInfo); + if (boundary.parentFlushed) { request.clientRenderedBoundaries.push(boundary); } @@ -3232,7 +3214,8 @@ function safelyEmitEarlyPreloads( ); } catch (error) { // We assume preloads are optimistic and thus non-fatal if errored. - logRecoverableError(request, error); + const errorInfo: ThrownInfo = {}; + logRecoverableError(request, error, errorInfo); } } @@ -3486,16 +3469,19 @@ function retryRenderTask( const trackedPostpones = request.trackedPostpones; task.abortSet.delete(task); const postponeInstance: Postpone = (x: any); - logPostpone(request, postponeInstance.message); + + const postponeInfo = getThrownInfo(task.componentStack); + logPostpone(request, postponeInstance.message, postponeInfo); trackPostpone(request, trackedPostpones, task, segment); finishedTask(request, task.blockedBoundary, segment); - lastBoundaryErrorComponentStackDev = null; return; } } + + const errorInfo = getThrownInfo(task.componentStack); task.abortSet.delete(task); segment.status = ERRORED; - erroredTask(request, task.blockedBoundary, x); + erroredTask(request, task.blockedBoundary, x, errorInfo); return; } finally { if (enableFloat) { @@ -3576,10 +3562,12 @@ function retryReplayTask(request: Request, task: ReplayTask): void { } task.replay.pendingTasks--; task.abortSet.delete(task); + const errorInfo = getThrownInfo(task.componentStack); erroredReplay( request, task.blockedBoundary, x, + errorInfo, task.replay.nodes, task.replay.slots, ); @@ -3637,7 +3625,8 @@ export function performWork(request: Request): void { flushCompletedQueues(request, request.destination); } } catch (error) { - logRecoverableError(request, error); + const errorInfo: ThrownInfo = {}; + logRecoverableError(request, error, errorInfo); fatalError(request, error); } finally { setCurrentResumableState(prevResumableState); @@ -4201,7 +4190,8 @@ export function startFlowing(request: Request, destination: Destination): void { try { flushCompletedQueues(request, destination); } catch (error) { - logRecoverableError(request, error); + const errorInfo: ThrownInfo = {}; + logRecoverableError(request, error, errorInfo); fatalError(request, error); } } @@ -4226,7 +4216,8 @@ export function abort(request: Request, reason: mixed): void { flushCompletedQueues(request, request.destination); } } catch (error) { - logRecoverableError(request, error); + const errorInfo: ThrownInfo = {}; + logRecoverableError(request, error, errorInfo); fatalError(request, error); } }