Skip to content

Commit 349a99a

Browse files
authored
Badge Environment Name on Thrown Errors from the Server (#29846)
When we replay logs we badge them with e.g. `[Server]`. That way it's easy to identify that the source of the log actually happened on the Server (RSC). However, when we threw an error we didn't have any such thing. The error was rethrown on the client and then handled just like any other client error. This transfers the `environmentName` in DEV to our restored Error "sub-class" (conceptually) along with `digest`. That way you can read `error.environmentName` to print this in your own UI. I also updated our default for `onCaughtError` (and `onError` in Fizz) to use the `printToConsole` helper that the Flight Client uses to log it with the badge format. So by default you get the same experience as console.error for caught errors: <img width="810" alt="Screenshot 2024-06-10 at 9 25 12 PM" src="https://github.com/facebook/react/assets/63648/8490fedc-09f6-4286-9332-fbe6b0faa2d3"> <img width="815" alt="Screenshot 2024-06-10 at 9 39 30 PM" src="https://github.com/facebook/react/assets/63648/bdcfc554-504a-4b1d-82bf-b717e74975ac"> Unfortunately I can't do the same thing for `onUncaughtError` nor `onRecoverableError` because they use `reportError` which doesn't have custom formatting (unless we also prevented default on window.onerror). However maybe that's ok because 1) you should always have an error boundary 2) it's not likely that an RSC error can actually recover because it's not going to be rendered again so shouldn't really happen outside some parent conditionally rendering maybe. The other problem with this approach is that the default is no longer trivial - so reimplementing the default in user space is trickier and ideally we shouldn't expose our default to be called.
1 parent 7045700 commit 349a99a

34 files changed

+134
-36
lines changed

packages/internal-test-utils/consoleMock.js

+11-6
Original file line numberDiff line numberDiff line change
@@ -418,13 +418,18 @@ export function createLogAssertion(
418418
let argIndex = 0;
419419
// console.* could have been called with a non-string e.g. `console.error(new Error())`
420420
// eslint-disable-next-line react-internal/safe-string-coercion
421-
String(format).replace(/%s/g, () => argIndex++);
421+
String(format).replace(/%s|%c/g, () => argIndex++);
422422
if (argIndex !== args.length) {
423-
logsMismatchingFormat.push({
424-
format,
425-
args,
426-
expectedArgCount: argIndex,
427-
});
423+
if (format.includes('%c%s')) {
424+
// We intentionally use mismatching formatting when printing badging because we don't know
425+
// the best default to use for different types because the default varies by platform.
426+
} else {
427+
logsMismatchingFormat.push({
428+
format,
429+
args,
430+
expectedArgCount: argIndex,
431+
});
432+
}
428433
}
429434

430435
// Check for extra component stacks

packages/internal-test-utils/shouldIgnoreConsoleError.js

+4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
module.exports = function shouldIgnoreConsoleError(format, args) {
44
if (__DEV__) {
55
if (typeof format === 'string') {
6+
if (format.startsWith('%c%s')) {
7+
// Looks like a badged error message
8+
args.splice(0, 3);
9+
}
610
if (
711
args[0] != null &&
812
((typeof args[0] === 'object' &&

packages/react-client/src/ReactFlightClientConsoleConfigBrowser.js renamed to packages/react-client/src/ReactClientConsoleConfigBrowser.js

+10-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
* @flow
88
*/
99

10+
import {warn, error} from 'shared/consoleWithStackDev';
11+
1012
const badgeFormat = '%c%s%c ';
1113
// Same badge styling as DevTools.
1214
const badgeStyle =
@@ -63,7 +65,12 @@ export function printToConsole(
6365
);
6466
}
6567

66-
// eslint-disable-next-line react-internal/no-production-logging
67-
console[methodName].apply(console, newArgs);
68-
return;
68+
if (methodName === 'error') {
69+
error.apply(console, newArgs);
70+
} else if (methodName === 'warn') {
71+
warn.apply(console, newArgs);
72+
} else {
73+
// eslint-disable-next-line react-internal/no-production-logging
74+
console[methodName].apply(console, newArgs);
75+
}
6976
}

packages/react-client/src/ReactFlightClientConsoleConfigPlain.js renamed to packages/react-client/src/ReactClientConsoleConfigPlain.js

+10-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
* @flow
88
*/
99

10+
import {warn, error} from 'shared/consoleWithStackDev';
11+
1012
const badgeFormat = '[%s] ';
1113
const pad = ' ';
1214

@@ -44,7 +46,12 @@ export function printToConsole(
4446
newArgs.splice(offset, 0, badgeFormat, pad + badgeName + pad);
4547
}
4648

47-
// eslint-disable-next-line react-internal/no-production-logging
48-
console[methodName].apply(console, newArgs);
49-
return;
49+
if (methodName === 'error') {
50+
error.apply(console, newArgs);
51+
} else if (methodName === 'warn') {
52+
warn.apply(console, newArgs);
53+
} else {
54+
// eslint-disable-next-line react-internal/no-production-logging
55+
console[methodName].apply(console, newArgs);
56+
}
5057
}

packages/react-client/src/ReactFlightClientConsoleConfigServer.js renamed to packages/react-client/src/ReactClientConsoleConfigServer.js

+10-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
* @flow
88
*/
99

10+
import {warn, error} from 'shared/consoleWithStackDev';
11+
1012
// This flips color using ANSI, then sets a color styling, then resets.
1113
const badgeFormat = '\x1b[0m\x1b[7m%c%s\x1b[0m%c ';
1214
// Same badge styling as DevTools.
@@ -64,7 +66,12 @@ export function printToConsole(
6466
);
6567
}
6668

67-
// eslint-disable-next-line react-internal/no-production-logging
68-
console[methodName].apply(console, newArgs);
69-
return;
69+
if (methodName === 'error') {
70+
error.apply(console, newArgs);
71+
} else if (methodName === 'warn') {
72+
warn.apply(console, newArgs);
73+
} else {
74+
// eslint-disable-next-line react-internal/no-production-logging
75+
console[methodName].apply(console, newArgs);
76+
}
7077
}

packages/react-client/src/ReactFlightClient.js

+5
Original file line numberDiff line numberDiff line change
@@ -1730,6 +1730,7 @@ function resolveErrorDev(
17301730
digest: string,
17311731
message: string,
17321732
stack: string,
1733+
env: string,
17331734
): void {
17341735
if (!__DEV__) {
17351736
// These errors should never make it into a build so we don't need to encode them in codes.json
@@ -1769,6 +1770,7 @@ function resolveErrorDev(
17691770
}
17701771

17711772
(error: any).digest = digest;
1773+
(error: any).environmentName = env;
17721774
const errorWithDigest: ErrorWithDigest = (error: any);
17731775
const chunks = response._chunks;
17741776
const chunk = chunks.get(id);
@@ -2056,6 +2058,8 @@ function resolveConsoleEntry(
20562058
task.run(callStack);
20572059
return;
20582060
}
2061+
// TODO: Set the current owner so that consoleWithStackDev adds the component
2062+
// stack during the replay - if needed.
20592063
}
20602064
const rootTask = response._debugRootTask;
20612065
if (rootTask != null) {
@@ -2198,6 +2202,7 @@ function processFullRow(
21982202
errorInfo.digest,
21992203
errorInfo.message,
22002204
errorInfo.stack,
2205+
errorInfo.env,
22012206
);
22022207
} else {
22032208
resolveErrorProd(response, id, errorInfo.digest);

packages/react-client/src/__tests__/ReactFlight-test.js

+2
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ describe('ReactFlight', () => {
127127
this.props.expectedMessage,
128128
);
129129
expect(this.state.error.digest).toBe('a dev digest');
130+
expect(this.state.error.environmentName).toBe('Server');
130131
} else {
131132
expect(this.state.error.message).toBe(
132133
'An error occurred in the Server Components render. The specific message is omitted in production' +
@@ -143,6 +144,7 @@ describe('ReactFlight', () => {
143144
expectedDigest = '[]';
144145
}
145146
expect(this.state.error.digest).toContain(expectedDigest);
147+
expect(this.state.error.environmentName).toBe(undefined);
146148
expect(this.state.error.stack).toBe(
147149
'Error: ' + this.state.error.message,
148150
);

packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-esm.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
11-
export * from 'react-client/src/ReactFlightClientConsoleConfigBrowser';
11+
export * from 'react-client/src/ReactClientConsoleConfigBrowser';
1212
export * from 'react-server-dom-esm/src/ReactFlightClientConfigBundlerESM';
1313
export * from 'react-server-dom-esm/src/ReactFlightClientConfigTargetESMBrowser';
1414
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-turbopack.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
11-
export * from 'react-client/src/ReactFlightClientConsoleConfigBrowser';
11+
export * from 'react-client/src/ReactClientConsoleConfigBrowser';
1212
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack';
1313
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackBrowser';
1414
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackBrowser';

packages/react-client/src/forks/ReactFlightClientConfig.dom-browser.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
11-
export * from 'react-client/src/ReactFlightClientConsoleConfigBrowser';
11+
export * from 'react-client/src/ReactClientConsoleConfigBrowser';
1212
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack';
1313
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackBrowser';
1414
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackBrowser';

packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
11-
export * from 'react-client/src/ReactFlightClientConsoleConfigPlain';
11+
export * from 'react-client/src/ReactClientConsoleConfigPlain';
1212
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
1313

1414
export type Response = any;

packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-turbopack.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
11-
export * from 'react-client/src/ReactFlightClientConsoleConfigServer';
11+
export * from 'react-client/src/ReactClientConsoleConfigServer';
1212
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack';
1313
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer';
1414
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer';

packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-webpack.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
11-
export * from 'react-client/src/ReactFlightClientConsoleConfigServer';
11+
export * from 'react-client/src/ReactClientConsoleConfigServer';
1212
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack';
1313
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer';
1414
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer';

packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
11-
export * from 'react-client/src/ReactFlightClientConsoleConfigBrowser';
11+
export * from 'react-client/src/ReactClientConsoleConfigBrowser';
1212
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
1313

1414
export type Response = any;

packages/react-client/src/forks/ReactFlightClientConfig.dom-node-esm.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
export * from 'react-client/src/ReactFlightClientStreamConfigNode';
11-
export * from 'react-client/src/ReactFlightClientConsoleConfigServer';
11+
export * from 'react-client/src/ReactClientConsoleConfigServer';
1212
export * from 'react-server-dom-esm/src/ReactFlightClientConfigBundlerESM';
1313
export * from 'react-server-dom-esm/src/ReactFlightClientConfigTargetESMServer';
1414
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack-bundled.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
export * from 'react-client/src/ReactFlightClientStreamConfigNode';
11-
export * from 'react-client/src/ReactFlightClientConsoleConfigServer';
11+
export * from 'react-client/src/ReactClientConsoleConfigServer';
1212
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack';
1313
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer';
1414
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer';

packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
export * from 'react-client/src/ReactFlightClientStreamConfigNode';
11-
export * from 'react-client/src/ReactFlightClientConsoleConfigServer';
11+
export * from 'react-client/src/ReactClientConsoleConfigServer';
1212
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerNode';
1313
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer';
1414
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

packages/react-client/src/forks/ReactFlightClientConfig.dom-node-webpack.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
export * from 'react-client/src/ReactFlightClientStreamConfigNode';
11-
export * from 'react-client/src/ReactFlightClientConsoleConfigServer';
11+
export * from 'react-client/src/ReactClientConsoleConfigServer';
1212
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack';
1313
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer';
1414
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer';

packages/react-client/src/forks/ReactFlightClientConfig.dom-node.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
export * from 'react-client/src/ReactFlightClientStreamConfigNode';
11-
export * from 'react-client/src/ReactFlightClientConsoleConfigServer';
11+
export * from 'react-client/src/ReactClientConsoleConfigServer';
1212
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerNode';
1313
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer';
1414
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

packages/react-noop-renderer/src/createReactNoop.js

+5
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
635635
NotPendingTransition: (null: TransitionStatus),
636636

637637
resetFormInstance(form: Instance) {},
638+
639+
printToConsole(methodName, args, badgeName) {
640+
// eslint-disable-next-line react-internal/no-production-logging
641+
console[methodName].apply(console, args);
642+
},
638643
};
639644

640645
const hostConfig = useMutation

packages/react-reconciler/src/ReactFiberErrorLogger.js

+29-7
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import ReactSharedInternals from 'shared/ReactSharedInternals';
2020

2121
import {enableOwnerStacks} from 'shared/ReactFeatureFlags';
2222

23+
import {printToConsole} from './ReactFiberConfig';
24+
2325
// Side-channel since I'm not sure we want to make this part of the public API
2426
let componentName: null | string = null;
2527
let errorBoundaryName: null | string = null;
@@ -94,13 +96,33 @@ export function defaultOnCaughtError(
9496
}.`;
9597

9698
if (enableOwnerStacks) {
97-
console.error(
98-
'%o\n\n%s\n\n%s\n',
99-
error,
100-
componentNameMessage,
101-
recreateMessage,
102-
// We let our consoleWithStackDev wrapper add the component stack to the end.
103-
);
99+
if (
100+
typeof error === 'object' &&
101+
error !== null &&
102+
typeof error.environmentName === 'string'
103+
) {
104+
// This was a Server error. We print the environment name in a badge just like we do with
105+
// replays of console logs to indicate that the source of this throw as actually the Server.
106+
printToConsole(
107+
'error',
108+
[
109+
'%o\n\n%s\n\n%s\n',
110+
error,
111+
componentNameMessage,
112+
recreateMessage,
113+
// We let our consoleWithStackDev wrapper add the component stack to the end.
114+
],
115+
error.environmentName,
116+
);
117+
} else {
118+
console.error(
119+
'%o\n\n%s\n\n%s\n',
120+
error,
121+
componentNameMessage,
122+
recreateMessage,
123+
// We let our consoleWithStackDev wrapper add the component stack to the end.
124+
);
125+
}
104126
} else {
105127
// The current Fiber is disconnected at this point which means that console printing
106128
// cannot add a component stack since it terminates at the deletion node. This is not

packages/react-reconciler/src/forks/ReactFiberConfig.art.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88
*/
99

1010
export * from 'react-art/src/ReactFiberConfigART';
11+
export * from 'react-client/src/ReactClientConsoleConfigBrowser';

packages/react-reconciler/src/forks/ReactFiberConfig.custom.js

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export const suspendInstance = $$$config.suspendInstance;
8080
export const waitForCommitToBeReady = $$$config.waitForCommitToBeReady;
8181
export const NotPendingTransition = $$$config.NotPendingTransition;
8282
export const resetFormInstance = $$$config.resetFormInstance;
83+
export const printToConsole = $$$config.printToConsole;
8384

8485
// -------------------
8586
// Microtasks

packages/react-reconciler/src/forks/ReactFiberConfig.dom.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88
*/
99

1010
export * from 'react-dom-bindings/src/client/ReactFiberConfigDOM';
11+
export * from 'react-client/src/ReactClientConsoleConfigBrowser';

packages/react-reconciler/src/forks/ReactFiberConfig.fabric.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88
*/
99

1010
export * from 'react-native-renderer/src/ReactFiberConfigFabric';
11+
export * from 'react-client/src/ReactClientConsoleConfigPlain';

packages/react-reconciler/src/forks/ReactFiberConfig.native.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88
*/
99

1010
export * from 'react-native-renderer/src/ReactFiberConfigNative';
11+
export * from 'react-client/src/ReactClientConsoleConfigPlain';

packages/react-reconciler/src/forks/ReactFiberConfig.test.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88
*/
99

1010
export * from 'react-test-renderer/src/ReactFiberConfigTestHost';
11+
export * from 'react-client/src/ReactClientConsoleConfigPlain';

packages/react-server/src/ReactFizzServer.js

+12-1
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ import {
7878
resetResumableState,
7979
completeResumableState,
8080
emitEarlyPreloads,
81+
printToConsole,
8182
} from './ReactFizzConfig';
8283
import {
8384
constructClassInstance,
@@ -363,7 +364,17 @@ export opaque type Request = {
363364
const DEFAULT_PROGRESSIVE_CHUNK_SIZE = 12800;
364365

365366
function defaultErrorHandler(error: mixed) {
366-
console['error'](error); // Don't transform to our wrapper
367+
if (
368+
typeof error === 'object' &&
369+
error !== null &&
370+
typeof error.environmentName === 'string'
371+
) {
372+
// This was a Server error. We print the environment name in a badge just like we do with
373+
// replays of console logs to indicate that the source of this throw as actually the Server.
374+
printToConsole('error', [error], error.environmentName);
375+
} else {
376+
console['error'](error); // Don't transform to our wrapper
377+
}
367378
return null;
368379
}
369380

0 commit comments

Comments
 (0)