Skip to content

Commit 2a911f2

Browse files
authored
[Flight] Send the awaited Promise to the client as additional debug information (#33592)
Stacked on #33588, #33589 and #33590. This lets us automatically show the resolved value in the UI. <img width="863" alt="Screenshot 2025-06-22 at 12 54 41 AM" src="https://github.com/user-attachments/assets/a66d1d5e-0513-4767-910c-5c7169fc2df4" /> We can also show rejected I/O that may or may not have been handled with the error message. <img width="838" alt="Screenshot 2025-06-22 at 12 55 06 AM" src="https://github.com/user-attachments/assets/e0a8b6ae-08ba-46d8-8cc5-efb60956a1d1" /> To get this working we need to keep the Promise around for longer so that we can access it once we want to emit an async sequence. I do this by storing the WeakRefs but to ensure that the Promise doesn't get garbage collected, I keep a WeakMap of Promise to the Promise that it depended on. This lets the VM still clean up any Promise chains that have leaves that are cleaned up. So this makes Promises live until the last Promise downstream is done. At that point we can go back up the chain to read the values out of them. Additionally, to get the best possible value we don't want to get a Promise that's used by internals of a third-party function. We want the value that the first party gets to observe. To do this I had to change the logic for which "await" to use, to be the one that is the first await that happened in user space. It's not enough that the await has any first party at all on the stack - it has to be the very first frame. This is a little sketchy because it relies on the `.then()` call or `await` call not having any third party wrappers. But it gives the best object since it hides all the internals. For example when you call `fetch()` we now log that actual `Response` object.
1 parent 18ee505 commit 2a911f2

File tree

9 files changed

+791
-296
lines changed

9 files changed

+791
-296
lines changed

fixtures/flight/src/App.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,22 @@ function Foo({children}) {
3333
return <div>{children}</div>;
3434
}
3535

36+
async function delayedError(text, ms) {
37+
return new Promise((_, reject) =>
38+
setTimeout(() => reject(new Error(text)), ms)
39+
);
40+
}
41+
3642
async function delay(text, ms) {
3743
return new Promise(resolve => setTimeout(() => resolve(text), ms));
3844
}
3945

4046
async function delayTwice() {
41-
await delay('', 20);
47+
try {
48+
await delayedError('Delayed exception', 20);
49+
} catch (x) {
50+
// Ignored
51+
}
4252
await delay('', 10);
4353
}
4454

@@ -113,6 +123,7 @@ async function ServerComponent({noCache}) {
113123
export default async function App({prerender, noCache}) {
114124
const res = await fetch('http://localhost:3001/todos');
115125
const todos = await res.json();
126+
console.log(res);
116127

117128
const dedupedChild = <ServerComponent noCache={noCache} />;
118129
const message = getServerState();

packages/react-client/src/ReactFlightClient.js

Lines changed: 77 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@ import {
7979
logDedupedComponentRender,
8080
logComponentErrored,
8181
logIOInfo,
82+
logIOInfoErrored,
8283
logComponentAwait,
84+
logComponentAwaitErrored,
8385
} from './ReactFlightPerformanceTrack';
8486

8587
import {
@@ -96,6 +98,8 @@ import {getOwnerStackByComponentInfoInDev} from 'shared/ReactComponentInfoStack'
9698

9799
import {injectInternals} from './ReactFlightClientDevToolsHook';
98100

101+
import {OMITTED_PROP_ERROR} from './ReactFlightPropertyAccess';
102+
99103
import ReactVersion from 'shared/ReactVersion';
100104

101105
import isArray from 'shared/isArray';
@@ -1684,11 +1688,7 @@ function parseModelString(
16841688
Object.defineProperty(parentObject, key, {
16851689
get: function () {
16861690
// TODO: We should ideally throw here to indicate a difference.
1687-
return (
1688-
'This object has been omitted by React in the console log ' +
1689-
'to avoid sending too much data from the server. Try logging smaller ' +
1690-
'or more specific objects.'
1691-
);
1691+
return OMITTED_PROP_ERROR;
16921692
},
16931693
enumerable: true,
16941694
configurable: false,
@@ -2909,7 +2909,29 @@ function initializeIOInfo(response: Response, ioInfo: ReactIOInfo): void {
29092909
// $FlowFixMe[cannot-write]
29102910
ioInfo.end += response._timeOrigin;
29112911

2912-
logIOInfo(ioInfo, response._rootEnvironmentName);
2912+
const env = response._rootEnvironmentName;
2913+
const promise = ioInfo.value;
2914+
if (promise) {
2915+
const thenable: Thenable<mixed> = (promise: any);
2916+
switch (thenable.status) {
2917+
case INITIALIZED:
2918+
logIOInfo(ioInfo, env, thenable.value);
2919+
break;
2920+
case ERRORED:
2921+
logIOInfoErrored(ioInfo, env, thenable.reason);
2922+
break;
2923+
default:
2924+
// If we haven't resolved the Promise yet, wait to log until have so we can include
2925+
// its data in the log.
2926+
promise.then(
2927+
logIOInfo.bind(null, ioInfo, env),
2928+
logIOInfoErrored.bind(null, ioInfo, env),
2929+
);
2930+
break;
2931+
}
2932+
} else {
2933+
logIOInfo(ioInfo, env, undefined);
2934+
}
29132935
}
29142936

29152937
function resolveIOInfo(
@@ -3193,13 +3215,55 @@ function flushComponentPerformance(
31933215
}
31943216
// $FlowFixMe: Refined.
31953217
const asyncInfo: ReactAsyncInfo = candidateInfo;
3196-
logComponentAwait(
3197-
asyncInfo,
3198-
trackIdx,
3199-
time,
3200-
endTime,
3201-
response._rootEnvironmentName,
3202-
);
3218+
const env = response._rootEnvironmentName;
3219+
const promise = asyncInfo.awaited.value;
3220+
if (promise) {
3221+
const thenable: Thenable<mixed> = (promise: any);
3222+
switch (thenable.status) {
3223+
case INITIALIZED:
3224+
logComponentAwait(
3225+
asyncInfo,
3226+
trackIdx,
3227+
time,
3228+
endTime,
3229+
env,
3230+
thenable.value,
3231+
);
3232+
break;
3233+
case ERRORED:
3234+
logComponentAwaitErrored(
3235+
asyncInfo,
3236+
trackIdx,
3237+
time,
3238+
endTime,
3239+
env,
3240+
thenable.reason,
3241+
);
3242+
break;
3243+
default:
3244+
// We assume that we should have received the data by now since this is logged at the
3245+
// end of the response stream. This is more sensitive to ordering so we don't wait
3246+
// to log it.
3247+
logComponentAwait(
3248+
asyncInfo,
3249+
trackIdx,
3250+
time,
3251+
endTime,
3252+
env,
3253+
undefined,
3254+
);
3255+
break;
3256+
}
3257+
} else {
3258+
logComponentAwait(
3259+
asyncInfo,
3260+
trackIdx,
3261+
time,
3262+
endTime,
3263+
env,
3264+
undefined,
3265+
);
3266+
}
32033267
}
32043268
}
32053269
}

0 commit comments

Comments
 (0)