diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 8b64fdab23880..8dab71c70852b 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -732,8 +732,32 @@ function createElement( // This is effectively the complete phase. initializingHandler = handler.parent; if (handler.errored) { - // TODO: Encode the error as Lazy. - throw handler.value; + // Something errored inside this Element's props. We can turn this Element + // into a Lazy so that we can still render up until that Lazy is rendered. + const erroredChunk: ErroredChunk> = createErrorChunk( + response, + handler.value, + ); + if (__DEV__) { + // Conceptually the error happened inside this Element but right before + // it was rendered. We don't have a client side component to render but + // we can add some DebugInfo to explain that this was conceptually a + // Server side error that errored inside this element. That way any stack + // traces will point to the nearest JSX that errored - e.g. during + // serialization. + const erroredComponent: ReactComponentInfo = { + name: getComponentNameFromType(element.type) || '', + owner: element._owner, + }; + if (enableOwnerStacks) { + // $FlowFixMe[cannot-write] + erroredComponent.stack = element._debugStack; + // $FlowFixMe[cannot-write] + erroredComponent.task = element._debugTask; + } + erroredChunk._debugInfo = [erroredComponent]; + } + return createLazyChunkWrapper(erroredChunk); } if (handler.deps > 0) { // We have blocked references inside this Element but we can turn this into @@ -861,12 +885,41 @@ function waitForReference( // Promise.all. return; } + const blockedValue = handler.value; handler.errored = true; handler.value = error; const chunk = handler.chunk; if (chunk === null || chunk.status !== BLOCKED) { return; } + + if (__DEV__) { + if ( + typeof blockedValue === 'object' && + blockedValue !== null && + blockedValue.$$typeof === REACT_ELEMENT_TYPE + ) { + const element = blockedValue; + // Conceptually the error happened inside this Element but right before + // it was rendered. We don't have a client side component to render but + // we can add some DebugInfo to explain that this was conceptually a + // Server side error that errored inside this element. That way any stack + // traces will point to the nearest JSX that errored - e.g. during + // serialization. + const erroredComponent: ReactComponentInfo = { + name: getComponentNameFromType(element.type) || '', + owner: element._owner, + }; + if (enableOwnerStacks) { + // $FlowFixMe[cannot-write] + erroredComponent.stack = element._debugStack; + // $FlowFixMe[cannot-write] + erroredComponent.task = element._debugTask; + } + chunk._debugInfo = [erroredComponent]; + } + } + triggerErrorOnChunk(chunk, error); } @@ -961,7 +1014,22 @@ function getOutlinedModel( case BLOCKED: return waitForReference(chunk, parentObject, key, response, map, path); default: - throw chunk.reason; + // This is an error. Instead of erroring directly, we're going to encode this on + // an initialization handler so that we can catch it at the nearest Element. + if (initializingHandler) { + initializingHandler.errored = true; + initializingHandler.value = chunk.reason; + } else { + initializingHandler = { + parent: null, + chunk: null, + value: chunk.reason, + deps: 0, + errored: true, + }; + } + // Placeholder + return (null: any); } } diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 54d66bdac10d0..5507d4cb6b136 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -1104,6 +1104,46 @@ describe('ReactFlight', () => { }); }); + it('should handle serialization errors in element inside error boundary', async () => { + const ClientErrorBoundary = clientReference(ErrorBoundary); + + const expectedStack = __DEV__ + ? '\n in div' + '\n in ErrorBoundary (at **)' + '\n in App' + : '\n in ErrorBoundary (at **)'; + + function App() { + return ( + +
+ + ); + } + + const transport = ReactNoopFlightServer.render(, { + onError(x) { + if (__DEV__) { + return 'a dev digest'; + } + if (x instanceof Error) { + return `digest("${x.message}")`; + } else if (Array.isArray(x)) { + return `digest([])`; + } else if (typeof x === 'object' && x !== null) { + return `digest({})`; + } + return `digest(${String(x)})`; + }, + }); + + await act(() => { + startTransition(() => { + ReactNoop.render(ReactNoopFlightClient.read(transport)); + }); + }); + }); + it('should include server components in warning stacks', async () => { function Component() { // Trigger key warning diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index acf4dbc0f4b05..ceecbdc2b918f 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -1039,7 +1039,9 @@ function renderFunctionComponent( owner: owner, }; if (enableOwnerStacks) { - (componentDebugInfo: any).stack = stack; + // $FlowFixMe[prop-missing] + // $FlowFixMe[cannot-write] + componentDebugInfo.stack = stack; } // We outline this model eagerly so that we can refer to by reference as an owner. // If we had a smarter way to dedupe we might not have to do this if there ends up @@ -2055,20 +2057,19 @@ function renderModel( task.keyPath = prevKeyPath; task.implicitSlot = prevImplicitSlot; + // Something errored. We'll still send everything we have up until this point. + request.pendingChunks++; + const errorId = request.nextChunkId++; + const digest = logRecoverableError(request, x); + emitErrorChunk(request, errorId, digest, x); if (wasReactNode) { - // Something errored. We'll still send everything we have up until this point. // We'll replace this element with a lazy reference that throws on the client // once it gets rendered. - request.pendingChunks++; - const errorId = request.nextChunkId++; - const digest = logRecoverableError(request, x); - emitErrorChunk(request, errorId, digest, x); return serializeLazyID(errorId); } - // Something errored but it was not in a React Node. There's no need to serialize - // it by value because it'll just error the whole parent row anyway so we can - // just stop any siblings and error the whole parent row. - throw x; + // If we don't know if it was a React Node we render a direct reference and let + // the client deal with it. + return serializeByValueID(errorId); } }