Skip to content

Commit c498bfc

Browse files
authored
[devtools] Allow inspecting cause, name, message, stack of Errors in props (#33023)
1 parent 8e9a5fc commit c498bfc

File tree

4 files changed

+100
-3
lines changed

4 files changed

+100
-3
lines changed

packages/react-devtools-shared/src/__tests__/inspectedElement-test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -906,8 +906,8 @@ describe('InspectedElement', () => {
906906
},
907907
"usedRejectedPromise": {
908908
"reason": Dehydrated {
909-
"preview_short": Error,
910-
"preview_long": Error,
909+
"preview_short": Error: test-error-do-not-surface,
910+
"preview_long": Error: test-error-do-not-surface,
911911
},
912912
},
913913
}

packages/react-devtools-shared/src/hydration.js

+63-1
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,7 @@ export function dehydrate(
397397
return object;
398398
}
399399

400-
case 'class_instance':
400+
case 'class_instance': {
401401
isPathAllowedCheck = isPathAllowed(path);
402402

403403
if (level >= LEVEL_THRESHOLD && !isPathAllowedCheck) {
@@ -433,7 +433,69 @@ export function dehydrate(
433433
unserializable.push(path);
434434

435435
return value;
436+
}
437+
case 'error': {
438+
isPathAllowedCheck = isPathAllowed(path);
439+
440+
if (level >= LEVEL_THRESHOLD && !isPathAllowedCheck) {
441+
return createDehydrated(type, true, data, cleaned, path);
442+
}
443+
444+
const value: Unserializable = {
445+
unserializable: true,
446+
type,
447+
readonly: true,
448+
preview_short: formatDataForPreview(data, false),
449+
preview_long: formatDataForPreview(data, true),
450+
name: data.name,
451+
};
452+
453+
// name, message, stack and cause are not enumerable yet still interesting.
454+
value.message = dehydrate(
455+
data.message,
456+
cleaned,
457+
unserializable,
458+
path.concat(['message']),
459+
isPathAllowed,
460+
isPathAllowedCheck ? 1 : level + 1,
461+
);
462+
value.stack = dehydrate(
463+
data.stack,
464+
cleaned,
465+
unserializable,
466+
path.concat(['stack']),
467+
isPathAllowed,
468+
isPathAllowedCheck ? 1 : level + 1,
469+
);
470+
471+
if ('cause' in data) {
472+
value.cause = dehydrate(
473+
data.cause,
474+
cleaned,
475+
unserializable,
476+
path.concat(['cause']),
477+
isPathAllowed,
478+
isPathAllowedCheck ? 1 : level + 1,
479+
);
480+
}
481+
482+
getAllEnumerableKeys(data).forEach(key => {
483+
const keyAsString = key.toString();
436484

485+
value[keyAsString] = dehydrate(
486+
data[key],
487+
cleaned,
488+
unserializable,
489+
path.concat([keyAsString]),
490+
isPathAllowed,
491+
isPathAllowedCheck ? 1 : level + 1,
492+
);
493+
});
494+
495+
unserializable.push(path);
496+
497+
return value;
498+
}
437499
case 'infinity':
438500
case 'nan':
439501
case 'undefined':

packages/react-devtools-shared/src/utils.js

+20
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,7 @@ export type DataType =
554554
| 'class_instance'
555555
| 'data_view'
556556
| 'date'
557+
| 'error'
557558
| 'function'
558559
| 'html_all_collection'
559560
| 'html_element'
@@ -573,6 +574,21 @@ export type DataType =
573574
| 'undefined'
574575
| 'unknown';
575576

577+
function isError(data: Object): boolean {
578+
// If it doesn't event look like an error, it won't be an actual error.
579+
if ('name' in data && 'message' in data) {
580+
while (data) {
581+
// $FlowFixMe[method-unbinding]
582+
if (Object.prototype.toString.call(data) === '[object Error]') {
583+
return true;
584+
}
585+
data = Object.getPrototypeOf(data);
586+
}
587+
}
588+
589+
return false;
590+
}
591+
576592
/**
577593
* Get a enhanced/artificial type string based on the object instance
578594
*/
@@ -634,6 +650,8 @@ export function getDataType(data: Object): DataType {
634650
return 'regexp';
635651
} else if (typeof data.then === 'function') {
636652
return 'thenable';
653+
} else if (isError(data)) {
654+
return 'error';
637655
} else {
638656
// $FlowFixMe[method-unbinding]
639657
const toStringValue = Object.prototype.toString.call(data);
@@ -996,6 +1014,8 @@ export function formatDataForPreview(
9961014
} else {
9971015
return '{…}';
9981016
}
1017+
case 'error':
1018+
return truncateForDisplay(String(data));
9991019
case 'boolean':
10001020
case 'number':
10011021
case 'infinity':

packages/react-devtools-shell/src/app/Hydration/index.js

+15
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,14 @@ const usedRejectedPromise = Promise.reject(
130130
new Error('test-error-do-not-surface'),
131131
);
132132

133+
class DigestError extends Error {
134+
digest: string;
135+
constructor(message: string, options: any, digest: string) {
136+
super(message, options);
137+
this.digest = digest;
138+
}
139+
}
140+
133141
export default function Hydration(): React.Node {
134142
return (
135143
<Fragment>
@@ -149,6 +157,13 @@ export default function Hydration(): React.Node {
149157
usedFulfilledRichPromise={usedFulfilledRichPromise}
150158
usedPendingPromise={usedPendingPromise}
151159
usedRejectedPromise={usedRejectedPromise}
160+
// eslint-disable-next-line react-internal/prod-error-codes
161+
error={new Error('test')}
162+
// eslint-disable-next-line react-internal/prod-error-codes
163+
errorWithCause={new Error('one', {cause: new TypeError('two')})}
164+
errorWithDigest={new DigestError('test', {}, 'some-digest')}
165+
// $FlowFixMe[cannot-resolve-name] Flow doesn't know about DOMException
166+
domexception={new DOMException('test')}
152167
/>
153168
<DeepHooks />
154169
</Fragment>

0 commit comments

Comments
 (0)