#37728·React

Bug: [Flight] Client throws `object null is not iterable` reviving an AggregateError whose inner errors have no stack frames

Author: atomyCreated Oct 1, 2026Updated Oct 1, 2026

In development, the Flight client throws TypeError: object null is not iterable when it revives an AggregateError whose inner errors have no stack frames. This happens even though the Server Component caught the error. The error only reaches the client through the async debug info (the awaited I/O's rejection value). After the throw, the stream stops being processed and the root never resolves.

React version: 19.3.0 (also 19.3.0-canary-7c6ac13e-20260929; same code in the React build vendored by Next.js 16.3.4 through 16.4.0-canary.55). Node 26.10.0.

Steps To Reproduce

  1. npm install [email protected] [email protected] [email protected] webpack
  2. Save the two files below.
  3. node --conditions react-server server.mjs > payload.txt && node client.mjs < payload.txt

Link to code example:

server.mjs:

javascript
import { createElement } from 'react';
import { renderToPipeableStream } from 'react-server-dom-webpack/server';

// Like errors created inside Node internals (e.g. a net ECONNREFUSED): no stack frames.
function errorWithoutFrames(message) {
  const error = new Error(message);
  error.stack = `Error: ${message}`;
  return error;
}

function load() {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject(new AggregateError([errorWithoutFrames('a')], 'all failed')), 10);
  });
}

async function App() {
  try {
    await load();
    return 'loaded';
  } catch (error) {
    return 'handled: ' + error.message;
  }
}

renderToPipeableStream(createElement(App), {}).pipe(process.stdout);

client.mjs:

javascript
import { createFromNodeStream } from 'react-server-dom-webpack/client';

process.on('uncaughtException', (error) => {
  console.log('uncaughtException:', error.stack.split('\n').slice(0, 4).join('\n'));
});

const root = await createFromNodeStream(process.stdin, { moduleMap: {}, serverModuleMap: null, moduleLoading: null });
console.log('root:', JSON.stringify(root));

The current behavior

uncaughtException: TypeError: object null is not iterable (cannot read property Symbol(Symbol.iterator))
    at AggregateError (<anonymous>)
    at Timeout._onTimeout (server.mjs:13:29)        <- fake server frame
    at resolveErrorDev (react-server-dom-webpack-client.node.development.js:3626:51)
Warning: Detected unsettled top-level await at client.mjs:7

The root promise never resolves.

How I hit it in practice: in a Next.js 16 app in dev, mysql2 connects to MYSQL_HOST=localhost while the database is down. Node's happy-eyeballs connect (autoSelectFamily) rejects with AggregateError [ECONNREFUSED]. Its inner errors come from Node internals, so they have no user frames. Our code catches that error and renders a fallback. Even so:

  • the SSR Flight client logs ⨯ uncaughtException: TypeError: object null is not iterable, and page requests hang for 20s to minutes;
  • Firefox logs Uncaught TypeError: can't convert null to object (its wording for AggregateError(null)). The stack is attributed to the server function that started the query, because the error is constructed inside the fake server call stack.

Production builds are not affected.

The expected behavior

The handled error doesn't break the stream: the root resolves to "handled: all failed", and the AggregateError is revived for DevTools (with its errors, or at least without throwing).

Analysis

Relevant payload rows:

7:{"name":"Error","message":"a","stack":"$Y","env":"Server"}
6:["$Z7"]
4:E{"digest":"","name":"AggregateError","message":"all failed","stack":[["Timeout._onTimeout","server.mjs",13,29,13,16,false]],"env":"Server","owner":null,"errors":"$6"}
  • Server: the debug serializer outlines each inner Error with { objectLimit: 2 * stack.length + 1 }. With zero frames the limit is 1, and the error-info object itself uses it up. The empty stack array is therefore emitted as a deferred "$Y" reference. With a real stack (for example new Error('a') created in user code) the repro passes.
  • Client: in resolveErrorDev, reviveModel(response, errorInfo.errors, errorInfo, 'errors') reaches chunk 6, which is blocked on the deferred $Y. It gets a null placeholder back. AggregateError.bind(null, revivedErrors, …) is then called right away through buildFakeCallStack, so it becomes AggregateError(null, …), which throws.

This came in with #36156 (transporting AggregateError.errors). Possible fixes: fall back to an empty array when revivedErrors isn't an array yet (and fill errors in once the reference resolves), and/or don't defer an empty stack array in the inner error's debug model.