diff --git a/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js b/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js index cc925dad312b0..522d211aeb06f 100644 --- a/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js +++ b/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js @@ -906,8 +906,8 @@ describe('InspectedElement', () => { }, "usedRejectedPromise": { "reason": Dehydrated { - "preview_short": Error, - "preview_long": Error, + "preview_short": Error: test-error-do-not-surface, + "preview_long": Error: test-error-do-not-surface, }, }, } diff --git a/packages/react-devtools-shared/src/hydration.js b/packages/react-devtools-shared/src/hydration.js index 3df47c68bd7d6..6075d1959d219 100644 --- a/packages/react-devtools-shared/src/hydration.js +++ b/packages/react-devtools-shared/src/hydration.js @@ -397,7 +397,7 @@ export function dehydrate( return object; } - case 'class_instance': + case 'class_instance': { isPathAllowedCheck = isPathAllowed(path); if (level >= LEVEL_THRESHOLD && !isPathAllowedCheck) { @@ -433,7 +433,69 @@ export function dehydrate( unserializable.push(path); return value; + } + case 'error': { + isPathAllowedCheck = isPathAllowed(path); + + if (level >= LEVEL_THRESHOLD && !isPathAllowedCheck) { + return createDehydrated(type, true, data, cleaned, path); + } + + const value: Unserializable = { + unserializable: true, + type, + readonly: true, + preview_short: formatDataForPreview(data, false), + preview_long: formatDataForPreview(data, true), + name: data.name, + }; + + // name, message, stack and cause are not enumerable yet still interesting. + value.message = dehydrate( + data.message, + cleaned, + unserializable, + path.concat(['message']), + isPathAllowed, + isPathAllowedCheck ? 1 : level + 1, + ); + value.stack = dehydrate( + data.stack, + cleaned, + unserializable, + path.concat(['stack']), + isPathAllowed, + isPathAllowedCheck ? 1 : level + 1, + ); + + if ('cause' in data) { + value.cause = dehydrate( + data.cause, + cleaned, + unserializable, + path.concat(['cause']), + isPathAllowed, + isPathAllowedCheck ? 1 : level + 1, + ); + } + + getAllEnumerableKeys(data).forEach(key => { + const keyAsString = key.toString(); + value[keyAsString] = dehydrate( + data[key], + cleaned, + unserializable, + path.concat([keyAsString]), + isPathAllowed, + isPathAllowedCheck ? 1 : level + 1, + ); + }); + + unserializable.push(path); + + return value; + } case 'infinity': case 'nan': case 'undefined': diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js index ebb6265b1d668..0536a821c7283 100644 --- a/packages/react-devtools-shared/src/utils.js +++ b/packages/react-devtools-shared/src/utils.js @@ -554,6 +554,7 @@ export type DataType = | 'class_instance' | 'data_view' | 'date' + | 'error' | 'function' | 'html_all_collection' | 'html_element' @@ -573,6 +574,21 @@ export type DataType = | 'undefined' | 'unknown'; +function isError(data: Object): boolean { + // If it doesn't event look like an error, it won't be an actual error. + if ('name' in data && 'message' in data) { + while (data) { + // $FlowFixMe[method-unbinding] + if (Object.prototype.toString.call(data) === '[object Error]') { + return true; + } + data = Object.getPrototypeOf(data); + } + } + + return false; +} + /** * Get a enhanced/artificial type string based on the object instance */ @@ -634,6 +650,8 @@ export function getDataType(data: Object): DataType { return 'regexp'; } else if (typeof data.then === 'function') { return 'thenable'; + } else if (isError(data)) { + return 'error'; } else { // $FlowFixMe[method-unbinding] const toStringValue = Object.prototype.toString.call(data); @@ -996,6 +1014,8 @@ export function formatDataForPreview( } else { return '{…}'; } + case 'error': + return truncateForDisplay(String(data)); case 'boolean': case 'number': case 'infinity': diff --git a/packages/react-devtools-shell/src/app/Hydration/index.js b/packages/react-devtools-shell/src/app/Hydration/index.js index 227f8558ef42b..f90bd6bef12ee 100644 --- a/packages/react-devtools-shell/src/app/Hydration/index.js +++ b/packages/react-devtools-shell/src/app/Hydration/index.js @@ -130,6 +130,14 @@ const usedRejectedPromise = Promise.reject( new Error('test-error-do-not-surface'), ); +class DigestError extends Error { + digest: string; + constructor(message: string, options: any, digest: string) { + super(message, options); + this.digest = digest; + } +} + export default function Hydration(): React.Node { return ( @@ -149,6 +157,13 @@ export default function Hydration(): React.Node { usedFulfilledRichPromise={usedFulfilledRichPromise} usedPendingPromise={usedPendingPromise} usedRejectedPromise={usedRejectedPromise} + // eslint-disable-next-line react-internal/prod-error-codes + error={new Error('test')} + // eslint-disable-next-line react-internal/prod-error-codes + errorWithCause={new Error('one', {cause: new TypeError('two')})} + errorWithDigest={new DigestError('test', {}, 'some-digest')} + // $FlowFixMe[cannot-resolve-name] Flow doesn't know about DOMException + domexception={new DOMException('test')} />