[Flight] Reduce risk of maximum call stack exceeded when emitting async sequence (#35159)
Sebastian "Sebbie" Silbermann committed
Nov 17, 2025 at 18:54 UTC
45bc3c9f04b96e1dbf23d5d0bbac435fc2bafcda
1 file changed
+23
-8
packages/react-server/src/ReactFlightServer.js
+23
-8
@@ -2347,7 +2347,8 @@ function visitAsyncNodeImpl(
2347
// The technique for debugging the effects of uncached data on the render is to simply uncache it.
2348
return null;
2349
}
2350
- let previousIONode = null;
2350
+
2351
+ let previousIONode: void | null | PromiseNode | IONode = null;
2352
// First visit anything that blocked this sequence to start in the first place.
2353
if (node.previous !== null) {
2354
previousIONode = visitAsyncNode(
@@ -2363,12 +2364,20 @@ function visitAsyncNodeImpl(
2364
return undefined;
2365
}
2366
}
2367
+
2368
+ // `found` represents the return value of the following switch statement.
2369
+ // We can't use multiple `return` statements in the switch statement
2370
+ // since that prevents Closure compiler from inlining `visitAsyncImpl`
2371
+ // thus doubling the call stack size.
2372
+ let found: void | null | PromiseNode | IONode;
2373
switch (node.tag) {
2374
case IO_NODE: {
2368
- return node;
2375
+ found = node;
2376
+ break;
2377
}
2378
case UNRESOLVED_PROMISE_NODE: {
2371
- return previousIONode;
2379
+ found = previousIONode;
2380
+ break;
2381
}
2382
case PROMISE_NODE: {
2383
const awaited = node.awaited;
@@ -2379,7 +2388,8 @@ function visitAsyncNodeImpl(
2388
if (ioNode === undefined) {
2389
// Undefined is used as a signal that we found a suitable aborted node and we don't have to find
2390
// further aborted nodes.
2382
- return undefined;
2391
+ found = undefined;
2392
+ break;
2393
} else if (ioNode !== null) {
2394
// This Promise was blocked on I/O. That's a signal that this Promise is interesting to log.
2395
// We don't log it yet though. We return it to be logged by the point where it's awaited.
@@ -2436,10 +2446,12 @@ function visitAsyncNodeImpl(
2446
forwardDebugInfo(request, task, debugInfo);
2447
}
2448
}
2439
- return match;
2449
+ found = match;
2450
+ break;
2451
}
2452
case UNRESOLVED_AWAIT_NODE: {
2442
- return previousIONode;
2453
+ found = previousIONode;
2454
+ break;
2455
}
2456
case AWAIT_NODE: {
2457
const awaited = node.awaited;
@@ -2449,7 +2461,8 @@ function visitAsyncNodeImpl(
2461
if (ioNode === undefined) {
2462
// Undefined is used as a signal that we found a suitable aborted node and we don't have to find
2463
// further aborted nodes.
2452
- return undefined;
2464
+ found = undefined;
2465
+ break;
2466
} else if (ioNode !== null) {
2467
const startTime: number = node.start;
2468
const endTime: number = node.end;
@@ -2545,13 +2558,15 @@ function visitAsyncNodeImpl(
2558
forwardDebugInfo(request, task, debugInfo);
2559
}
2560
}
2548
- return match;
2561
+ found = match;
2562
+ break;
2563
}
2564
default: {
2565
// eslint-disable-next-line react-internal/prod-error-codes
2566
throw new Error('Unknown AsyncSequence tag. This is a bug in React.');
2567
}
2568
}
2569
+ return found;
2570
}
2571
2572
function emitAsyncSequence(