Repository: sindresorhus/serialize-error
Pull request #73 (2022-05-05 14:43:52)

Actually serialize nested Errors
- Fixes https://github.com/sindresorhus/serialize-error/issues/74
- For https://github.com/pixiebrix/pixiebrix-extension/pull/3336


Patch:
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,
 		});
 	}
 


Test patch:
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 <anonymous>:1:13',