@samitouri / QOS-React / commits / 74568e8627

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