Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Actually serialize nested Errors #73

Merged
merged 5 commits into from
May 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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