[Flight] Add support for transporting `Error.cause` (#35810)
Sebastian "Sebbie" Silbermann committed
Feb 19, 2026 at 15:50 UTC
2ba3065527cbabc9778363e78a411653cd4cd215
4 files changed
+185
-18
packages/react-client/src/ReactFlightClient.js
+14
-8
@@ -8,6 +8,7 @@
8
*/
9
10
import type {
11
+ JSONValue,
12
Thenable,
13
ReactDebugInfo,
14
ReactDebugInfoEntry,
@@ -132,14 +133,6 @@ interface FlightStreamController {
133
134
type UninitializedModel = string;
135
135
-export type JSONValue =
136
- | number
137
- | null
138
- | boolean
139
- | string
140
- | {+[key: string]: JSONValue}
141
- | $ReadOnlyArray<JSONValue>;
142
-
136
type ProfilingResult = {
137
track: number,
138
endTime: number,
@@ -3527,6 +3520,18 @@ function resolveErrorDev(
3520
}
3521
3522
let error;
3523
+ const errorOptions =
3524
+ 'cause' in errorInfo
3525
+ ? {
3526
+ cause: reviveModel(
3527
+ response,
3528
+ // $FlowFixMe[incompatible-cast] -- Flow thinks `cause` in `cause?: JSONValue` can be undefined after `in` check.
3529
+ (errorInfo.cause: JSONValue),
3530
+ errorInfo,
3531
+ 'cause',
3532
+ ),
3533
+ }
3534
+ : undefined;
3535
const callStack = buildFakeCallStack(
3536
response,
3537
stack,
@@ -3537,6 +3542,7 @@ function resolveErrorDev(
3542
null,
3543
message ||
3544
'An error occurred in the Server Components render but no message was provided',
3545
+ errorOptions,
3546
),
3547
);
3548
packages/react-client/src/__tests__/ReactFlight-test.js
+133
@@ -707,6 +707,139 @@ describe('ReactFlight', () => {
707
}
708
});
709
710
+ it('can transport Error.cause', async () => {
711
+ function renderError(error) {
712
+ if (!(error instanceof Error)) {
713
+ return `${JSON.stringify(error)}`;
714
+ }
715
+ return `
716
+ is error: ${error instanceof Error}
717
+ name: ${error.name}
718
+ message: ${error.message}
719
+ stack: ${normalizeCodeLocInfo(error.stack).split('\n').slice(0, 2).join('\n')}
720
+ environmentName: ${error.environmentName}
721
+ cause: ${'cause' in error ? renderError(error.cause) : 'no cause'}`;
722
+ }
723
+ function ComponentClient({error}) {
724
+ return renderError(error);
725
+ }
726
+ const Component = clientReference(ComponentClient);
727
+
728
+ function ServerComponent() {
729
+ const cause = new TypeError('root cause', {
730
+ cause: {type: 'object cause'},
731
+ });
732
+ const error = new Error('hello', {cause});
733
+ return <Component error={error} />;
734
+ }
735
+
736
+ const transport = ReactNoopFlightServer.render(<ServerComponent />, {
737
+ onError(x) {
738
+ if (__DEV__) {
739
+ return 'a dev digest';
740
+ }
741
+ return `digest("${x.message}")`;
742
+ },
743
+ });
744
+
745
+ await act(() => {
746
+ ReactNoop.render(ReactNoopFlightClient.read(transport));
747
+ });
748
+
749
+ if (__DEV__) {
750
+ expect(ReactNoop).toMatchRenderedOutput(`
751
+ is error: true
752
+ name: Error
753
+ message: hello
754
+ stack: Error: hello
755
+ in ServerComponent (at **)
756
+ environmentName: Server
757
+ cause:
758
+ is error: true
759
+ name: TypeError
760
+ message: root cause
761
+ stack: TypeError: root cause
762
+ in ServerComponent (at **)
763
+ environmentName: Server
764
+ cause: {"type":"object cause"}`);
765
+ } else {
766
+ expect(ReactNoop).toMatchRenderedOutput(`
767
+ is error: true
768
+ name: Error
769
+ 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.
770
+ 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.
771
+ environmentName: undefined
772
+ cause: no cause`);
773
+ }
774
+ });
775
+
776
+ it('includes Error.cause in thrown errors', async () => {
777
+ function renderError(error) {
778
+ if (!(error instanceof Error)) {
779
+ return `${JSON.stringify(error)}`;
780
+ }
781
+ return `
782
+ is error: true
783
+ name: ${error.name}
784
+ message: ${error.message}
785
+ stack: ${normalizeCodeLocInfo(error.stack).split('\n').slice(0, 2).join('\n')}
786
+ environmentName: ${error.environmentName}
787
+ cause: ${'cause' in error ? renderError(error.cause) : 'no cause'}`;
788
+ }
789
+
790
+ function ServerComponent() {
791
+ const cause = new TypeError('root cause', {
792
+ cause: {type: 'object cause'},
793
+ });
794
+ const error = new Error('hello', {cause});
795
+ throw error;
796
+ }
797
+
798
+ const transport = ReactNoopFlightServer.render(<ServerComponent />, {
799
+ onError(x) {
800
+ if (__DEV__) {
801
+ return 'a dev digest';
802
+ }
803
+ return `digest("${x.message}")`;
804
+ },
805
+ });
806
+
807
+ let error;
808
+ try {
809
+ await act(() => {
810
+ ReactNoop.render(ReactNoopFlightClient.read(transport));
811
+ });
812
+ } catch (x) {
813
+ error = x;
814
+ }
815
+
816
+ if (__DEV__) {
817
+ expect(renderError(error)).toEqual(`
818
+ is error: true
819
+ name: Error
820
+ message: hello
821
+ stack: Error: hello
822
+ in ServerComponent (at **)
823
+ environmentName: Server
824
+ cause:
825
+ is error: true
826
+ name: TypeError
827
+ message: root cause
828
+ stack: TypeError: root cause
829
+ in ServerComponent (at **)
830
+ environmentName: Server
831
+ cause: {"type":"object cause"}`);
832
+ } else {
833
+ expect(renderError(error)).toEqual(`
834
+ is error: true
835
+ name: Error
836
+ 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.
837
+ 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.
838
+ environmentName: undefined
839
+ cause: no cause`);
840
+ }
841
+ });
842
+
843
it('can transport cyclic objects', async () => {
844
function ComponentClient({prop}) {
845
expect(prop.obj.obj.obj).toBe(prop.obj.obj);
packages/react-server/src/ReactFlightServer.js
+29
-10
@@ -467,14 +467,6 @@ function getCurrentStackInDEV(): string {
467
468
const ObjectPrototype = Object.prototype;
469
470
-type JSONValue =
471
- | string
472
- | boolean
473
- | number
474
- | null
475
- | {+[key: string]: JSONValue}
476
- | $ReadOnlyArray<JSONValue>;
477
-
470
const stringify = JSON.stringify;
471
472
type ReactJSONValue =
@@ -498,6 +490,7 @@ export type ReactClientValue =
490
| React$Element<string>
491
| React$Element<ClientReference<any> & any>
492
| ReactComponentInfo
493
+ | ReactErrorInfo
494
| string
495
| boolean
496
| number
@@ -4171,6 +4164,11 @@ function serializeErrorValue(request: Request, error: Error): string {
4164
stack = [];
4165
}
4166
const errorInfo: ReactErrorInfoDev = {name, message, stack, env};
4167
+ if ('cause' in error) {
4168
+ const cause: ReactClientValue = (error.cause: any);
4169
+ const causeId = outlineModel(request, cause);
4170
+ errorInfo.cause = serializeByValueID(causeId);
4171
+ }
4172
const id = outlineModel(request, errorInfo);
4173
return '$Z' + id.toString(16);
4174
} else {
@@ -4181,7 +4179,11 @@ function serializeErrorValue(request: Request, error: Error): string {
4179
}
4180
}
4181
4184
-function serializeDebugErrorValue(request: Request, error: Error): string {
4182
+function serializeDebugErrorValue(
4183
+ request: Request,
4184
+ counter: {objectLimit: number},
4185
+ error: Error,
4186
+): string {
4187
if (__DEV__) {
4188
let name: string = 'Error';
4189
let message: string;
@@ -4203,6 +4205,12 @@ function serializeDebugErrorValue(request: Request, error: Error): string {
4205
stack = [];
4206
}
4207
const errorInfo: ReactErrorInfoDev = {name, message, stack, env};
4208
+ if ('cause' in error) {
4209
+ counter.objectLimit--;
4210
+ const cause: ReactClientValue = (error.cause: any);
4211
+ const causeId = outlineDebugModel(request, counter, cause);
4212
+ errorInfo.cause = serializeByValueID(causeId);
4213
+ }
4214
const id = outlineDebugModel(
4215
request,
4216
{objectLimit: stack.length * 2 + 1},
@@ -4231,6 +4239,7 @@ function emitErrorChunk(
4239
let message: string;
4240
let stack: ReactStackTrace;
4241
let env = (0, request.environmentName)();
4242
+ let causeReference: null | string = null;
4243
try {
4244
if (error instanceof Error) {
4245
name = error.name;
@@ -4243,6 +4252,13 @@ function emitErrorChunk(
4252
// Keep the environment name.
4253
env = errorEnv;
4254
}
4255
+ if ('cause' in error) {
4256
+ const cause: ReactClientValue = (error.cause: any);
4257
+ const causeId = debug
4258
+ ? outlineDebugModel(request, {objectLimit: 5}, cause)
4259
+ : outlineModel(request, cause);
4260
+ causeReference = serializeByValueID(causeId);
4261
+ }
4262
} else if (typeof error === 'object' && error !== null) {
4263
message = describeObjectForErrorMessage(error);
4264
stack = [];
@@ -4258,6 +4274,9 @@ function emitErrorChunk(
4274
const ownerRef =
4275
owner == null ? null : outlineComponentInfo(request, owner);
4276
errorInfo = {digest, name, message, stack, env, owner: ownerRef};
4277
+ if (causeReference !== null) {
4278
+ (errorInfo: ReactErrorInfoDev).cause = causeReference;
4279
+ }
4280
} else {
4281
errorInfo = {digest};
4282
}
@@ -4969,7 +4988,7 @@ function renderDebugModel(
4988
return serializeDebugFormData(request, value);
4989
}
4990
if (value instanceof Error) {
4972
- return serializeDebugErrorValue(request, value);
4991
+ return serializeDebugErrorValue(request, counter, value);
4992
}
4993
if (value instanceof ArrayBuffer) {
4994
return serializeDebugTypedArray(request, 'A', new Uint8Array(value));
packages/shared/ReactTypes.js
+9
@@ -228,6 +228,14 @@ export type ReactErrorInfoProd = {
228
+digest: string,
229
};
230
231
+export type JSONValue =
232
+ | string
233
+ | boolean
234
+ | number
235
+ | null
236
+ | {+[key: string]: JSONValue}
237
+ | $ReadOnlyArray<JSONValue>;
238
+
239
export type ReactErrorInfoDev = {
240
+digest?: string,
241
+name: string,
@@ -235,6 +243,7 @@ export type ReactErrorInfoDev = {
243
+stack: ReactStackTrace,
244
+env: string,
245
+owner?: null | string,
246
+ cause?: JSONValue,
247
};
248
249
export type ReactErrorInfo = ReactErrorInfoProd | ReactErrorInfoDev;