Bug: [Flight] Client throws `object null is not iterable` reviving an AggregateError whose inner errors have no stack frames
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
npm install [email protected] [email protected] [email protected] webpack- Save the two files below.
node --conditions react-server server.mjs > payload.txt && node client.mjs < payload.txt
Link to code example:
server.mjs:
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:
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:7The 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 forAggregateError(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
Errorwith{ objectLimit: 2 * stack.length + 1 }. With zero frames the limit is 1, and the error-info object itself uses it up. The emptystackarray is therefore emitted as a deferred"$Y"reference. With a real stack (for examplenew 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 anullplaceholder back.AggregateError.bind(null, revivedErrors, …)is then called right away throughbuildFakeCallStack, so it becomesAggregateError(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.
Source: facebook/react