Skip to content

Commit

Permalink
Actually serialize nested errors (#73)
Browse files Browse the repository at this point in the history
  • Loading branch information
fregante authored May 12, 2022
1 parent 6f6102f commit 0af9fe8
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 22 deletions.
44 changes: 26 additions & 18 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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
Expand All @@ -110,7 +116,7 @@ const destroyCircular = ({

if (!seen.includes(from[key])) {
depth++;
to[key] = destroyLocal(from[key]);
to[key] = continueDestroyCircular(from[key]);

continue;
}
Expand All @@ -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,
Expand All @@ -146,6 +152,7 @@ export function serializeError(value, options = {}) {
maxDepth,
depth: 0,
useToJSON,
serialize: true,
});
}

Expand All @@ -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,
});
}

Expand Down
31 changes: 27 additions & 4 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -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 <anonymous>:1:13',
Expand Down

0 comments on commit 0af9fe8

Please sign in to comment.