Skip to content

Commit

Permalink
[Flight] Transport custom error names in dev mode (#32116)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
unstubbable authored Jan 17, 2025
1 parent fd2d279 commit 829401d
Show file tree
Hide file tree
Showing 3 changed files with 24 additions and 4 deletions.
10 changes: 9 additions & 1 deletion packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -2156,6 +2163,7 @@ function resolveErrorDev(
error = callStack();
}

(error: any).name = name;
(error: any).environmentName = env;
return error;
}
Expand Down
14 changes: 12 additions & 2 deletions packages/react-client/src/__tests__/ReactFlight-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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} />;
}

Expand All @@ -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
Expand Down
4 changes: 3 additions & 1 deletion packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 {
Expand Down

0 comments on commit 829401d

Please sign in to comment.