From 0af9fe8578e1b88f93dffd25df04be42777df3b6 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Thu, 12 May 2022 12:39:06 +0800 Subject: [PATCH] Actually serialize nested errors (#73) --- index.js | 44 ++++++++++++++++++++++++++------------------ test.js | 31 +++++++++++++++++++++++++++---- 2 files changed, 53 insertions(+), 22 deletions(-) diff --git a/index.js b/index.js index 2a74412..9e5e7b3 100644 --- a/index.js +++ b/index.js @@ -54,13 +54,23 @@ const getErrorConstructor = name => errorConstructors.get(name) ?? Error; const destroyCircular = ({ from, seen, - to_, + to, forceEnumerable, maxDepth, depth, useToJSON, + serialize, }) => { - const to = to_ ?? (Array.isArray(from) ? [] : {}); + if (!to) { + if (Array.isArray(from)) { + to = []; + } else if (!serialize && isErrorLike(from)) { + const Error = getErrorConstructor(from.name); + to = new Error(); + } else { + to = {}; + } + } seen.push(from); @@ -72,19 +82,15 @@ const destroyCircular = ({ return toJSON(from); } - const destroyLocal = value => { - const Error = getErrorConstructor(value.name); - return destroyCircular({ - from: value, - seen: [...seen], - - to_: isErrorLike(value) ? new Error() : undefined, - forceEnumerable, - maxDepth, - depth, - useToJSON, - }); - }; + const continueDestroyCircular = value => destroyCircular({ + from: value, + seen: [...seen], + forceEnumerable, + maxDepth, + depth, + useToJSON, + serialize, + }); for (const [key, value] of Object.entries(from)) { // eslint-disable-next-line node/prefer-global/buffer @@ -110,7 +116,7 @@ const destroyCircular = ({ if (!seen.includes(from[key])) { depth++; - to[key] = destroyLocal(from[key]); + to[key] = continueDestroyCircular(from[key]); continue; } @@ -121,7 +127,7 @@ const destroyCircular = ({ for (const {property, enumerable} of commonProperties) { if (typeof from[property] !== 'undefined' && from[property] !== null) { Object.defineProperty(to, property, { - value: isErrorLike(from[property]) ? destroyLocal(from[property]) : from[property], + value: isErrorLike(from[property]) ? continueDestroyCircular(from[property]) : from[property], enumerable: forceEnumerable ? true : enumerable, configurable: true, writable: true, @@ -146,6 +152,7 @@ export function serializeError(value, options = {}) { maxDepth, depth: 0, useToJSON, + serialize: true, }); } @@ -170,9 +177,10 @@ export function deserializeError(value, options = {}) { return destroyCircular({ from: value, seen: [], - to_: new Error(), + to: new Error(), maxDepth, depth: 0, + serialize: false, }); } diff --git a/test.js b/test.js index f8c512d..7b46b8f 100644 --- a/test.js +++ b/test.js @@ -11,6 +11,15 @@ function deserializeNonError(t, value) { t.is(deserialized.message, JSON.stringify(value)); } +// TODO: Replace with plain `new Error('outer', {cause: new Error('inner')})` when targeting Node 16.9+ +function setErrorCause(error, cause) { + Object.defineProperty(error, 'cause', { + value: cause, + enumerable: false, + writable: true, + }); +} + test('main', t => { const serialized = serializeError(new Error('foo')); const properties = Object.keys(serialized); @@ -132,16 +141,30 @@ test('should serialize nested errors', t => { const serialized = serializeError(error); t.is(serialized.message, 'outer error'); - t.is(serialized.innerError.message, 'inner error'); + t.like(serialized.innerError, { + name: 'Error', + message: 'inner error', + }); + t.false(serialized.innerError instanceof Error); }); test('should serialize the cause property', t => { const error = new Error('outer error'); - error.cause = new Error('inner error'); + setErrorCause(error, new Error('inner error')); + setErrorCause(error.cause, new Error('deeper error')); const serialized = serializeError(error); t.is(serialized.message, 'outer error'); - t.is(serialized.cause.message, 'inner error'); + t.like(serialized.cause, { + name: 'Error', + message: 'inner error', + cause: { + name: 'Error', + message: 'deeper error', + }, + }); + t.false(serialized.cause instanceof Error); + t.false(serialized.cause.cause instanceof Error); }); test('should handle top-level null values', t => { @@ -237,7 +260,7 @@ for (const property of ['cause', 'any']) { }); } -test('deserialized name, stack, cause an message should not be enumerable, other props should be', t => { +test('deserialized name, stack, cause and message should not be enumerable, other props should be', t => { const object = { message: 'error message', stack: 'at :1:13',