[Flight] Transport `AggregateErrors.errors` (#36156)
Sebastian "Sebbie" Silbermann committed
Mar 28, 2026 at 18:18 UTC
74568e8627aa43469b74f2972f427a209639d0b6
10 files changed
+262
-13
.eslintrc.js
+1
@@ -566,6 +566,7 @@ module.exports = {
566
CallSite: 'readonly',
567
ConsoleTask: 'readonly', // TOOD: Figure out what the official name of this will be.
568
ReturnType: 'readonly',
569
+ AggregateError: 'readonly',
570
AnimationFrameID: 'readonly',
571
WeakRef: 'readonly',
572
// For Flow type annotation. Only `BigInt` is valid at runtime.
packages/internal-test-utils/ReactInternalTestUtils.js
-1
@@ -122,7 +122,6 @@ ${diff(expectedLog, actualLog)}
122
123
function aggregateErrors(errors: Array<mixed>): mixed {
124
if (errors.length > 1 && typeof AggregateError === 'function') {
125
- // eslint-disable-next-line no-undef
125
return new AggregateError(errors);
126
}
127
return errors[0];
packages/internal-test-utils/internalAct.js
-1
@@ -34,7 +34,6 @@ async function waitForMicrotasks() {
34
35
function aggregateErrors(errors: Array<mixed>): mixed {
36
if (errors.length > 1 && typeof AggregateError === 'function') {
37
- // eslint-disable-next-line no-undef
37
return new AggregateError(errors);
38
}
39
return errors[0];
packages/react-client/src/ReactFlightClient.js
+31
-8
@@ -3525,7 +3525,8 @@ function resolveErrorDev(
3525
3526
let error;
3527
const errorOptions =
3528
- 'cause' in errorInfo
3528
+ // We don't serialize Error.cause in prod so we never need to deserialize
3529
+ __DEV__ && 'cause' in errorInfo
3530
? {
3531
cause: reviveModel(
3532
response,
@@ -3536,18 +3537,40 @@ function resolveErrorDev(
3537
),
3538
}
3539
: undefined;
3540
+ const isAggregateError =
3541
+ typeof AggregateError !== 'undefined' && 'errors' in errorInfo;
3542
+ const revivedErrors =
3543
+ // We don't serialize AggregateError.errors in prod so we never need to deserialize
3544
+ __DEV__ && isAggregateError
3545
+ ? reviveModel(
3546
+ response,
3547
+ // $FlowFixMe[incompatible-cast]
3548
+ (errorInfo.errors: JSONValue),
3549
+ errorInfo,
3550
+ 'errors',
3551
+ )
3552
+ : null;
3553
const callStack = buildFakeCallStack(
3554
response,
3555
stack,
3556
env,
3557
false,
3544
- // $FlowFixMe[incompatible-use]
3545
- Error.bind(
3546
- null,
3547
- message ||
3548
- 'An error occurred in the Server Components render but no message was provided',
3549
- errorOptions,
3550
- ),
3558
+ isAggregateError
3559
+ ? // $FlowFixMe[incompatible-use]
3560
+ AggregateError.bind(
3561
+ null,
3562
+ revivedErrors,
3563
+ message ||
3564
+ 'An error occurred in the Server Components render but no message was provided',
3565
+ errorOptions,
3566
+ )
3567
+ : // $FlowFixMe[incompatible-use]
3568
+ Error.bind(
3569
+ null,
3570
+ message ||
3571
+ 'An error occurred in the Server Components render but no message was provided',
3572
+ errorOptions,
3573
+ ),
3574
);
3575
3576
let ownerTask: null | ConsoleTask = null;
packages/react-client/src/__tests__/ReactFlight-test.js
+198
@@ -840,6 +840,204 @@ describe('ReactFlight', () => {
840
}
841
});
842
843
+ it('can transport AggregateError', async () => {
844
+ function renderError(error) {
845
+ if (!(error instanceof Error)) {
846
+ return `${JSON.stringify(error)}`;
847
+ }
848
+ let result = `
849
+ is error: ${error instanceof AggregateError ? 'AggregateError' : 'Error'}
850
+ name: ${error.name}
851
+ message: ${error.message}
852
+ stack: ${normalizeCodeLocInfo(error.stack).split('\n').slice(0, 2).join('\n')}
853
+ environmentName: ${error.environmentName}
854
+ cause: ${'cause' in error ? renderError(error.cause) : 'no cause'}`;
855
+ if ('errors' in error) {
856
+ result += `
857
+ errors: [${error.errors.map(e => renderError(e)).join(',\n')}]`;
858
+ }
859
+ return result;
860
+ }
861
+ function ComponentClient({error}) {
862
+ return renderError(error);
863
+ }
864
+ const Component = clientReference(ComponentClient);
865
+
866
+ function ServerComponent() {
867
+ const error1 = new TypeError('first error');
868
+ const error2 = new RangeError('second error');
869
+ const error = new AggregateError([error1, error2], 'aggregate');
870
+ return <Component error={error} />;
871
+ }
872
+
873
+ const transport = ReactNoopFlightServer.render(<ServerComponent />, {
874
+ onError(x) {
875
+ if (__DEV__) {
876
+ return 'a dev digest';
877
+ }
878
+ return `digest("${x.message}")`;
879
+ },
880
+ });
881
+
882
+ await act(() => {
883
+ ReactNoop.render(ReactNoopFlightClient.read(transport));
884
+ });
885
+
886
+ if (__DEV__) {
887
+ expect(ReactNoop).toMatchRenderedOutput(`
888
+ is error: AggregateError
889
+ name: AggregateError
890
+ message: aggregate
891
+ stack: AggregateError: aggregate
892
+ in ServerComponent (at **)
893
+ environmentName: Server
894
+ cause: no cause
895
+ errors: [
896
+ is error: Error
897
+ name: TypeError
898
+ message: first error
899
+ stack: TypeError: first error
900
+ in ServerComponent (at **)
901
+ environmentName: Server
902
+ cause: no cause,
903
+
904
+ is error: Error
905
+ name: RangeError
906
+ message: second error
907
+ stack: RangeError: second error
908
+ in ServerComponent (at **)
909
+ environmentName: Server
910
+ cause: no cause]`);
911
+ } else {
912
+ expect(ReactNoop).toMatchRenderedOutput(`
913
+ is error: Error
914
+ name: Error
915
+ message: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.
916
+ stack: Error: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.
917
+ environmentName: undefined
918
+ cause: no cause`);
919
+ }
920
+ });
921
+
922
+ it('includes AggregateError.errors in thrown errors', async () => {
923
+ function renderError(error) {
924
+ if (!(error instanceof Error)) {
925
+ return `${JSON.stringify(error)}`;
926
+ }
927
+ let result = `
928
+ is error: ${error instanceof AggregateError ? 'AggregateError' : 'Error'}
929
+ name: ${error.name}
930
+ message: ${error.message}
931
+ stack: ${normalizeCodeLocInfo(error.stack).split('\n').slice(0, 2).join('\n')}
932
+ environmentName: ${error.environmentName}
933
+ cause: ${'cause' in error ? renderError(error.cause) : 'no cause'}`;
934
+ if ('errors' in error) {
935
+ result += `
936
+ errors: [${error.errors.map(e => renderError(e)).join(',\n')}]`;
937
+ }
938
+ return result;
939
+ }
940
+
941
+ function ServerComponent() {
942
+ const error1 = new TypeError('first error');
943
+ const error2 = new RangeError('second error');
944
+ const error3 = new Error('third error');
945
+ const error4 = new Error('fourth error');
946
+ const error5 = new Error('fifth error');
947
+ const error6 = new Error('sixth error');
948
+ const error = new AggregateError(
949
+ [error1, error2, error3, error4, error5, error6],
950
+ 'aggregate',
951
+ );
952
+ throw error;
953
+ }
954
+
955
+ const transport = ReactNoopFlightServer.render(<ServerComponent />, {
956
+ onError(x) {
957
+ if (__DEV__) {
958
+ return 'a dev digest';
959
+ }
960
+ return `digest("${x.message}")`;
961
+ },
962
+ });
963
+
964
+ let error;
965
+ try {
966
+ await act(() => {
967
+ ReactNoop.render(ReactNoopFlightClient.read(transport));
968
+ });
969
+ } catch (x) {
970
+ error = x;
971
+ }
972
+
973
+ if (__DEV__) {
974
+ expect(renderError(error)).toEqual(`
975
+ is error: AggregateError
976
+ name: AggregateError
977
+ message: aggregate
978
+ stack: AggregateError: aggregate
979
+ in ServerComponent (at **)
980
+ environmentName: Server
981
+ cause: no cause
982
+ errors: [
983
+ is error: Error
984
+ name: TypeError
985
+ message: first error
986
+ stack: TypeError: first error
987
+ in ServerComponent (at **)
988
+ environmentName: Server
989
+ cause: no cause,
990
+
991
+ is error: Error
992
+ name: RangeError
993
+ message: second error
994
+ stack: RangeError: second error
995
+ in ServerComponent (at **)
996
+ environmentName: Server
997
+ cause: no cause,
998
+
999
+ is error: Error
1000
+ name: Error
1001
+ message: third error
1002
+ stack: Error: third error
1003
+ in ServerComponent (at **)
1004
+ environmentName: Server
1005
+ cause: no cause,
1006
+
1007
+ is error: Error
1008
+ name: Error
1009
+ message: fourth error
1010
+ stack: Error: fourth error
1011
+ in ServerComponent (at **)
1012
+ environmentName: Server
1013
+ cause: no cause,
1014
+
1015
+ is error: Error
1016
+ name: Error
1017
+ message: fifth error
1018
+ stack: Error: fifth error
1019
+ in ServerComponent (at **)
1020
+ environmentName: Server
1021
+ cause: no cause,
1022
+
1023
+ is error: Error
1024
+ name: Error
1025
+ message: sixth error
1026
+ stack: Error: sixth error
1027
+ in ServerComponent (at **)
1028
+ environmentName: Server
1029
+ cause: no cause]`);
1030
+ } else {
1031
+ expect(renderError(error)).toEqual(`
1032
+ is error: Error
1033
+ name: Error
1034
+ message: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.
1035
+ stack: Error: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.
1036
+ environmentName: undefined
1037
+ cause: no cause`);
1038
+ }
1039
+ });
1040
+
1041
it('can transport cyclic objects', async () => {
1042
function ComponentClient({prop}) {
1043
expect(prop.obj.obj.obj).toBe(prop.obj.obj);
packages/react-dom/src/__tests__/ReactDOMSelect-test.js
-1
@@ -1485,7 +1485,6 @@ describe('ReactDOMSelect', () => {
1485
);
1486
}),
1487
).rejects.toThrowError(
1488
- // eslint-disable-next-line no-undef
1488
new AggregateError([
1489
new TypeError('prod message'),
1490
new TypeError('prod message'),
packages/react-reconciler/src/__tests__/ReactFlushSync-test.js
-1
@@ -337,7 +337,6 @@ describe('ReactFlushSync', () => {
337
expect(getVisibleChildren(container3)).toEqual('aww');
338
339
// Because there were multiple errors, React threw an AggregateError.
340
- // eslint-disable-next-line no-undef
340
expect(error).toBeInstanceOf(AggregateError);
341
expect(error.errors.length).toBe(2);
342
expect(error.errors[0]).toBe(aahh);
packages/react-server/src/ReactFlightServer.js
+31
@@ -4169,6 +4169,14 @@ function serializeErrorValue(request: Request, error: Error): string {
4169
const causeId = outlineModel(request, cause);
4170
errorInfo.cause = serializeByValueID(causeId);
4171
}
4172
+ if (
4173
+ typeof AggregateError !== 'undefined' &&
4174
+ error instanceof AggregateError
4175
+ ) {
4176
+ const errors: ReactClientValue = (error.errors: any);
4177
+ const errorsId = outlineModel(request, errors);
4178
+ errorInfo.errors = serializeByValueID(errorsId);
4179
+ }
4180
const id = outlineModel(request, errorInfo);
4181
return '$Z' + id.toString(16);
4182
} else {
@@ -4211,6 +4219,15 @@ function serializeDebugErrorValue(
4219
const causeId = outlineDebugModel(request, counter, cause);
4220
errorInfo.cause = serializeByValueID(causeId);
4221
}
4222
+ if (
4223
+ typeof AggregateError !== 'undefined' &&
4224
+ error instanceof AggregateError
4225
+ ) {
4226
+ counter.objectLimit--;
4227
+ const errors: ReactClientValue = (error.errors: any);
4228
+ const errorsId = outlineDebugModel(request, counter, errors);
4229
+ errorInfo.errors = serializeByValueID(errorsId);
4230
+ }
4231
const id = outlineDebugModel(
4232
request,
4233
{objectLimit: stack.length * 2 + 1},
@@ -4240,6 +4257,7 @@ function emitErrorChunk(
4257
let stack: ReactStackTrace;
4258
let env = (0, request.environmentName)();
4259
let causeReference: null | string = null;
4260
+ let errorsReference: null | string = null;
4261
try {
4262
if (error instanceof Error) {
4263
name = error.name;
@@ -4259,6 +4277,16 @@ function emitErrorChunk(
4277
: outlineModel(request, cause);
4278
causeReference = serializeByValueID(causeId);
4279
}
4280
+ if (
4281
+ typeof AggregateError !== 'undefined' &&
4282
+ error instanceof AggregateError
4283
+ ) {
4284
+ const errors: ReactClientValue = (error.errors: any);
4285
+ const errorsId = debug
4286
+ ? outlineDebugModel(request, {objectLimit: 5}, errors)
4287
+ : outlineModel(request, errors);
4288
+ errorsReference = serializeByValueID(errorsId);
4289
+ }
4290
} else if (typeof error === 'object' && error !== null) {
4291
message = describeObjectForErrorMessage(error);
4292
stack = [];
@@ -4277,6 +4305,9 @@ function emitErrorChunk(
4305
if (causeReference !== null) {
4306
(errorInfo: ReactErrorInfoDev).cause = causeReference;
4307
}
4308
+ if (errorsReference !== null) {
4309
+ (errorInfo: ReactErrorInfoDev).errors = errorsReference;
4310
+ }
4311
} else {
4312
errorInfo = {digest};
4313
}
packages/react/src/ReactAct.js
-1
@@ -23,7 +23,6 @@ let didWarnNoAwaitAct = false;
23
24
function aggregateErrors(errors: Array<mixed>): mixed {
25
if (errors.length > 1 && typeof AggregateError === 'function') {
26
- // eslint-disable-next-line no-undef
26
return new AggregateError(errors);
27
}
28
return errors[0];
packages/shared/ReactTypes.js
+1
@@ -244,6 +244,7 @@ export type ReactErrorInfoDev = {
244
+env: string,
245
+owner?: null | string,
246
cause?: JSONValue,
247
+ errors?: JSONValue,
248
};
249
250
export type ReactErrorInfo = ReactErrorInfoProd | ReactErrorInfoDev;