@samitouri / QOS-React / commits / 829401dc17

[Flight] Transport custom error names in dev mode (#32116)

Typed errors is not a feature that Flight currently supports. However, for presentation purposes, serializing a custom error name is something we could support today. With this PR, we're now transporting custom error names through the server-client boundary, so that they are available in the client e.g. for console replaying. One example where this can be useful is when you want to print debug information while leveraging the fact that `console.warn` displays the error stack, including handling of hiding and source mapping stack frames. In this case you may want to show `Warning: ...` or `Debug: ...` instead of `Error: ...`. In prod mode, we still transport an obfuscated error that uses the default `Error` name, to not leak any sensitive information from the server to the client. This also means that you must not rely on the error name to discriminate errors, e.g. when handling them in an error boundary.

Hendrik Liebau committed Jan 17, 2025 at 23:48 UTC 829401dc173d79994a3401fce24084670f55fb5c
3 files changed +24 -4
packages/react-client/src/ReactFlightClient.js
+9 -1
@@ -2123,8 +2123,15 @@ function resolveErrorProd(response: Response): Error {
2123
2124 function resolveErrorDev(
2125 response: Response,
2126 - errorInfo: {message: string, stack: ReactStackTrace, env: string, ...},
2126 + errorInfo: {
2127 + name: string,
2128 + message: string,
2129 + stack: ReactStackTrace,
2130 + env: string,
2131 + ...
2132 + },
2133 ): Error {
2134 + const name: string = errorInfo.name;
2135 const message: string = errorInfo.message;
2136 const stack: ReactStackTrace = errorInfo.stack;
2137 const env: string = errorInfo.env;
@@ -2156,6 +2163,7 @@ function resolveErrorDev(
2163 error = callStack();
2164 }
2165
2166 + (error: any).name = name;
2167 (error: any).environmentName = env;
2168 return error;
2169 }
packages/react-client/src/__tests__/ReactFlight-test.js
+12 -2
@@ -694,9 +694,17 @@ describe('ReactFlight', () => {
694 });
695
696 it('can transport Error objects as values', async () => {
697 + class CustomError extends Error {
698 + constructor(message) {
699 + super(message);
700 + this.name = 'Custom';
701 + }
702 + }
703 +
704 function ComponentClient({prop}) {
705 return `
706 is error: ${prop instanceof Error}
707 + name: ${prop.name}
708 message: ${prop.message}
709 stack: ${normalizeCodeLocInfo(prop.stack).split('\n').slice(0, 2).join('\n')}
710 environmentName: ${prop.environmentName}
@@ -705,7 +713,7 @@ describe('ReactFlight', () => {
713 const Component = clientReference(ComponentClient);
714
715 function ServerComponent() {
708 - const error = new Error('hello');
716 + const error = new CustomError('hello');
717 return <Component prop={error} />;
718 }
719
@@ -718,14 +726,16 @@ describe('ReactFlight', () => {
726 if (__DEV__) {
727 expect(ReactNoop).toMatchRenderedOutput(`
728 is error: true
729 + name: Custom
730 message: hello
722 - stack: Error: hello
731 + stack: Custom: hello
732 in ServerComponent (at **)
733 environmentName: Server
734 `);
735 } else {
736 expect(ReactNoop).toMatchRenderedOutput(`
737 is error: true
738 + name: Error
739 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.
740 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.
741 environmentName: undefined
packages/react-server/src/ReactFlightServer.js
+3 -1
@@ -3093,10 +3093,12 @@ function emitPostponeChunk(
3093
3094 function serializeErrorValue(request: Request, error: Error): string {
3095 if (__DEV__) {
3096 + let name;
3097 let message;
3098 let stack: ReactStackTrace;
3099 let env = (0, request.environmentName)();
3100 try {
3101 + name = error.name;
3102 // eslint-disable-next-line react-internal/safe-string-coercion
3103 message = String(error.message);
3104 stack = filterStackTrace(request, error, 0);
@@ -3110,7 +3112,7 @@ function serializeErrorValue(request: Request, error: Error): string {
3112 message = 'An error occurred but serializing the error message failed.';
3113 stack = [];
3114 }
3113 - const errorInfo = {message, stack, env};
3115 + const errorInfo = {name, message, stack, env};
3116 const id = outlineModel(request, errorInfo);
3117 return '$Z' + id.toString(16);
3118 } else {