@samitouri / QOS-React-2 / commits / 45bc3c9f04

[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(