Repository: facebook/react
Pull request: [Flight] Clear chunk reason after successful module initialization

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()`.

Changed files:
filename: packages/react-client/src/ReactFlightClient.js
patch: @@ -1040,6 +1040,8 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): 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<T>(chunk: ResolvedModelChunk<T>): void {
     const initializedChunk: InitializedChunk<T> = (chunk: any);
     initializedChunk.status = INITIALIZED;
     initializedChunk.value = value;
+    initializedChunk.reason = null;
 
     if (__DEV__) {
       processChunkDebugInfo(response, initializedChunk, value);
@@ -1097,6 +1100,7 @@ function initializeModuleChunk<T>(chunk: ResolvedModuleChunk<T>): void {
     const initializedChunk: InitializedChunk<T> = (chunk: any);
     initializedChunk.status = INITIALIZED;
     initializedChunk.value = value;
+    initializedChunk.reason = null;
   } catch (error) {
     const erroredChunk: ErroredChunk<T> = (chunk: any);
     erroredChunk.status = ERRORED;

filename: packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
patch: @@ -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 <div>hello from client component</div>;
+    }
+    const ClientComponent = clientExports(MyComponent);
+
+    let resolveAsyncComponent;
+    async function AsyncComponent() {
+      await new Promise(r => {
+        resolveAsyncComponent = r;
+      });
+      return null;
+    }
+
+    function ServerComponent() {
+      return (
+        <>
+          <ClientComponent />
+          <Suspense>
+            <AsyncComponent />
+          </Suspense>
+        </>
+      );
+    }
+
+    const {writable: flightWritable, readable: flightReadable} =
+      getTestStream();
+    const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
+
+    const {pipe} = await serverAct(() =>
+      ReactServerDOMServer.renderToPipeableStream(
+        <ServerComponent />,
+        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(<App />).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 = [];
 

filename: packages/react-server/src/ReactFlightReplyServer.js
patch: @@ -478,6 +478,7 @@ function loadServerReference<A: Iterable<any>, T>(
       const initializedPromise: InitializedChunk<T> = (blockedPromise: any);
       initializedPromise.status = INITIALIZED;
       initializedPromise.value = resolvedValue;
+      initializedPromise.reason = null;
       return resolvedValue;
     }
   } else if (bound instanceof ReactPromise) {