From bc646840c7a6f0fd64f569925c5f006f3408d98d Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Thu, 12 Mar 2026 13:54:26 +0100 Subject: [PATCH 1/2] [Flight] Clear chunk reason after successful module initialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When `requireModule` triggers a reentrant `readChunk` on the same module chunk, the reentrant call can fail and set `chunk.reason` to an error. After the outer `requireModule` succeeds, the chunk transitions to initialized but retains the stale error as `reason`. When the Flight response stream later closes, it iterates all chunks and expects `reason` on initialized chunks to be a `FlightStreamController`. Since the stale `reason` is an `Error` object instead, calling `chunk.reason.error()` crashes with `TypeError: chunk.reason.error is not a function`. The reentrancy can occur when module evaluation synchronously triggers `readChunk` on the same chunk — for example, when code called during evaluation tries to resolve the client reference for the module that is currently being initialized. In Fizz SSR, `captureOwnerStack()` can trigger this because it constructs component stacks that resolve lazy client references via `readChunk`. The reentrant `requireModule` call returns the module's namespace object, but since the module is still being evaluated, accessing the export binding throws a TDZ (Temporal Dead Zone) `ReferenceError`. This sets the chunk to the errored state, and the `ReferenceError` becomes the stale `chunk.reason` after the outer call succeeds. This scenario is triggered in Next.js when a client module calls an instrumented API like `Math.random()` in module scope, which synchronously invokes `captureOwnerStack()`. --- .../react-client/src/ReactFlightClient.js | 1 + .../src/__tests__/ReactFlightDOM-test.js | 89 +++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 098a1a687e3a..a908278ce383 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -1097,6 +1097,7 @@ function initializeModuleChunk(chunk: ResolvedModuleChunk): void { const initializedChunk: InitializedChunk = (chunk: any); initializedChunk.status = INITIALIZED; initializedChunk.value = value; + initializedChunk.reason = null; } catch (error) { const erroredChunk: ErroredChunk = (chunk: any); erroredChunk.status = ERRORED; diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js index 94a5ac94546a..4abc18843050 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -1418,6 +1418,95 @@ describe('ReactFlightDOM', () => { expect(reportedErrors).toEqual([]); }); + it('should not retain stale error reason after reentrant module chunk initialization', async () => { + function MyComponent() { + return
hello from client component
; + } + const ClientComponent = clientExports(MyComponent); + + let resolveAsyncComponent; + async function AsyncComponent() { + await new Promise(r => { + resolveAsyncComponent = r; + }); + return null; + } + + function ServerComponent() { + return ( + <> + + + + + + ); + } + + const {writable: flightWritable, readable: flightReadable} = + getTestStream(); + const {writable: fizzWritable, readable: fizzReadable} = getTestStream(); + + const {pipe} = await serverAct(() => + ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ), + ); + pipe(flightWritable); + + let response = null; + function getResponse() { + if (response === null) { + response = + ReactServerDOMClient.createFromReadableStream(flightReadable); + } + return response; + } + + // Simulate a module that calls captureOwnerStack() during evaluation. + // In Fizz SSR, this causes a reentrant readChunk on the same module chunk. + // The reentrant require throws a TDZ error. + let evaluatingModuleId = null; + const origRequire = global.__webpack_require__; + global.__webpack_require__ = function (id) { + if (id === evaluatingModuleId) { + throw new ReferenceError( + "Cannot access 'MyComponent' before initialization", + ); + } + const result = origRequire(id); + if (result === MyComponent) { + evaluatingModuleId = id; + if (__DEV__) { + React.captureOwnerStack(); + } + evaluatingModuleId = null; + } + return result; + }; + + function App() { + return use(getResponse()); + } + + await serverAct(async () => { + ReactDOMFizzServer.renderToPipeableStream().pipe(fizzWritable); + }); + + global.__webpack_require__ = origRequire; + + // Resolve the async component so the Flight stream closes after the client + // module chunk was initialized. + await serverAct(async () => { + resolveAsyncComponent(); + }); + + const container = document.createElement('div'); + await readInto(container, fizzReadable); + expect(container.innerHTML).toContain('hello from client component'); + }); + it('should be able to recover from a direct reference erroring server-side', async () => { const reportedErrors = []; From b43ee2a3d98863a2ea6dcf63d1b66e0b93a59cd2 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Thu, 12 Mar 2026 17:47:48 +0100 Subject: [PATCH 2/2] Handle the two remaining cases --- packages/react-client/src/ReactFlightClient.js | 3 +++ packages/react-server/src/ReactFlightReplyServer.js | 1 + 2 files changed, 4 insertions(+) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index a908278ce383..fa89cf69ad67 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -1040,6 +1040,8 @@ function initializeModelChunk(chunk: ResolvedModelChunk): void { // Initialize any debug info and block the initializing chunk on any // unresolved entries. initializeDebugChunk(response, chunk); + // TODO: The chunk might have transitioned to ERRORED now. + // Should we return early if that happens? } try { @@ -1075,6 +1077,7 @@ function initializeModelChunk(chunk: ResolvedModelChunk): void { const initializedChunk: InitializedChunk = (chunk: any); initializedChunk.status = INITIALIZED; initializedChunk.value = value; + initializedChunk.reason = null; if (__DEV__) { processChunkDebugInfo(response, initializedChunk, value); diff --git a/packages/react-server/src/ReactFlightReplyServer.js b/packages/react-server/src/ReactFlightReplyServer.js index d3eff13ff465..21ff08a8aa02 100644 --- a/packages/react-server/src/ReactFlightReplyServer.js +++ b/packages/react-server/src/ReactFlightReplyServer.js @@ -478,6 +478,7 @@ function loadServerReference, T>( const initializedPromise: InitializedChunk = (blockedPromise: any); initializedPromise.status = INITIALIZED; initializedPromise.value = resolvedValue; + initializedPromise.reason = null; return resolvedValue; } } else if (bound instanceof ReactPromise) {