Repository: facebook/react
Pull request: [Flight] Transport custom error names in dev mode

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.

Changed files:
filename: packages/react-client/src/ReactFlightClient.js
patch: @@ -2123,8 +2123,15 @@ function resolveErrorProd(response: Response): Error {
 
 function resolveErrorDev(
   response: Response,
-  errorInfo: {message: string, stack: ReactStackTrace, env: string, ...},
+  errorInfo: {
+    name: string,
+    message: string,
+    stack: ReactStackTrace,
+    env: string,
+    ...
+  },
 ): Error {
+  const name: string = errorInfo.name;
   const message: string = errorInfo.message;
   const stack: ReactStackTrace = errorInfo.stack;
   const env: string = errorInfo.env;
@@ -2156,6 +2163,7 @@ function resolveErrorDev(
     error = callStack();
   }
 
+  (error: any).name = name;
   (error: any).environmentName = env;
   return error;
 }

filename: packages/react-client/src/__tests__/ReactFlight-test.js
patch: @@ -694,9 +694,17 @@ describe('ReactFlight', () => {
   });
 
   it('can transport Error objects as values', async () => {
+    class CustomError extends Error {
+      constructor(message) {
+        super(message);
+        this.name = 'Custom';
+      }
+    }
+
     function ComponentClient({prop}) {
       return `
         is error: ${prop instanceof Error}
+        name: ${prop.name}
         message: ${prop.message}
         stack: ${normalizeCodeLocInfo(prop.stack).split('\n').slice(0, 2).join('\n')}
         environmentName: ${prop.environmentName}
@@ -705,7 +713,7 @@ describe('ReactFlight', () => {
     const Component = clientReference(ComponentClient);
 
     function ServerComponent() {
-      const error = new Error('hello');
+      const error = new CustomError('hello');
       return <Component prop={error} />;
     }
 
@@ -718,14 +726,16 @@ describe('ReactFlight', () => {
     if (__DEV__) {
       expect(ReactNoop).toMatchRenderedOutput(`
         is error: true
+        name: Custom
         message: hello
-        stack: Error: hello
+        stack: Custom: hello
     in ServerComponent (at **)
         environmentName: Server
       `);
     } else {
       expect(ReactNoop).toMatchRenderedOutput(`
         is error: true
+        name: Error
         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.
         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.
         environmentName: undefined

filename: packages/react-server/src/ReactFlightServer.js
patch: @@ -3093,10 +3093,12 @@ function emitPostponeChunk(
 
 function serializeErrorValue(request: Request, error: Error): string {
   if (__DEV__) {
+    let name;
     let message;
     let stack: ReactStackTrace;
     let env = (0, request.environmentName)();
     try {
+      name = error.name;
       // eslint-disable-next-line react-internal/safe-string-coercion
       message = String(error.message);
       stack = filterStackTrace(request, error, 0);
@@ -3110,7 +3112,7 @@ function serializeErrorValue(request: Request, error: Error): string {
       message = 'An error occurred but serializing the error message failed.';
       stack = [];
     }
-    const errorInfo = {message, stack, env};
+    const errorInfo = {name, message, stack, env};
     const id = outlineModel(request, errorInfo);
     return '$Z' + id.toString(16);
   } else {