Repository: sindresorhus/serialize-error
Pull request #43 (2021-04-13 00:54:42)

Add `maxDepth` option
Fixes https://github.com/sindresorhus/serialize-error/issues/16


Patch:
diff --git a/index.d.ts b/index.d.ts
index 21eab41..1a3eb6b 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -7,6 +7,29 @@ export type ErrorObject = {
 	code?: string;
 } & JsonObject;
 
+export interface Options {
+	/**
+	The maximum depth of properties to preserve when serializing/deserializing.
+
+	@default Number.POSITIVE_INFINITY
+
+	@example
+	```
+	import {serializeError} from 'serialize-error';
+
+	const error = new Error('🦄');
+	error.one = {two: {three: {}}};
+
+	console.log(serializeError(error, {maxDepth: 1}));
+	//=> {name: 'Error', message: '…', one: {}}
+
+	console.log(serializeError(error, {maxDepth: 2}));
+	//=> {name: 'Error', message: '…', one: { two: {}}}
+	```
+	*/
+	readonly maxDepth?: number;
+}
+
 /**
 Serialize an `Error` object into a plain object.
 
@@ -55,7 +78,7 @@ console.log(serializeError(error));
 // => {date: '1970-01-01T00:00:00.000Z', message: '🦄', name, stack}
 ```
 */
-export function serializeError<ErrorType>(error: ErrorType): ErrorType extends Primitive
+export function serializeError<ErrorType>(error: ErrorType, options?: Options): ErrorType extends Primitive
 	? ErrorType
 	: ErrorObject;
 
@@ -83,4 +106,4 @@ console.log(error);
 // at <anonymous>:1:13
 ```
 */
-export function deserializeError(errorObject: ErrorObject | unknown): Error;
+export function deserializeError(errorObject: ErrorObject | unknown, options?: Options): Error;
diff --git a/index.js b/index.js
index e8a2926..a6ca592 100644
--- a/index.js
+++ b/index.js
@@ -43,12 +43,18 @@ const destroyCircular = ({
 	from,
 	seen,
 	to_,
-	forceEnumerable
+	forceEnumerable,
+	maxDepth,
+	depth
 }) => {
 	const to = to_ || (Array.isArray(from) ? [] : {});
 
 	seen.push(from);
 
+	if (depth >= maxDepth) {
+		return to;
+	}
+
 	if (typeof from.toJSON === 'function' && from[isCalled] !== true) {
 		return toJSON(from);
 	}
@@ -69,10 +75,14 @@ const destroyCircular = ({
 		}
 
 		if (!seen.includes(from[key])) {
+			depth++;
+
 			to[key] = destroyCircular({
 				from: from[key],
 				seen: seen.slice(),
-				forceEnumerable
+				forceEnumerable,
+				maxDepth,
+				depth
 			});
 			continue;
 		}
@@ -94,12 +104,16 @@ const destroyCircular = ({
 	return to;
 };
 
-const serializeError = value => {
+const serializeError = (value, options = {}) => {
+	const {maxDepth = Number.POSITIVE_INFINITY} = options;
+
 	if (typeof value === 'object' && value !== null) {
 		return destroyCircular({
 			from: value,
 			seen: [],
-			forceEnumerable: true
+			forceEnumerable: true,
+			maxDepth,
+			depth: 0
 		});
 	}
 
@@ -112,14 +126,22 @@ const serializeError = value => {
 	return value;
 };
 
-const deserializeError = value => {
+const deserializeError = (value, options = {}) => {
+	const {maxDepth = Number.POSITIVE_INFINITY} = options;
+
 	if (value instanceof Error) {
 		return value;
 	}
 
 	if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
 		const newError = new Error(); // eslint-disable-line unicorn/error-message
-		destroyCircular({from: value, seen: [], to_: newError});
+		destroyCircular({
+			from: value,
+			seen: [],
+			to_: newError,
+			maxDepth,
+			depth: 0
+		});
 		return newError;
 	}
 
diff --git a/readme.md b/readme.md
index f2c0769..51a847e 100644
--- a/readme.md
+++ b/readme.md
@@ -33,7 +33,7 @@ console.log(deserialized);
 
 ## API
 
-### serializeError(value)
+### serializeError(value, options?)
 
 Type: `Error | unknown`
 
@@ -76,7 +76,7 @@ console.log(serializeError(error));
 // => {date: '1970-01-01T00:00:00.000Z', message: '🦄', name, stack}
 ```
 
-### deserializeError(value)
+### deserializeError(value, options?)
 
 Type: `{[key: string]: unknown} | unknown`
 
@@ -86,3 +86,27 @@ Deserialize a plain object or any value into an `Error` object.
 Non-error values are wrapped in a `NonError` error.
 Custom properties are preserved.
 Circular references are handled.
+
+### options
+
+Type: `object`
+
+#### maxDepth
+
+Type: `number`\
+Default: `Number.POSITIVE_INFINITY`
+
+The maximum depth of properties to preserve when serializing/deserializing.
+
+```js
+const {serializeError} = require('serialize-error');
+
+const error = new Error('🦄');
+error.one = {two: {three: {}}};
+
+console.log(serializeError(error, {maxDepth: 1}));
+//=> {name: 'Error', message: '…', one: {}}
+
+console.log(serializeError(error, {maxDepth: 2}));
+//=> {name: 'Error', message: '…', one: { two: {}}}
+```


Test patch:
diff --git a/index.test-d.ts b/index.test-d.ts
index ad6ebd1..b9c544e 100644
--- a/index.test-d.ts
+++ b/index.test-d.ts
@@ -1,10 +1,11 @@
-import {expectType} from 'tsd';
-import {serializeError, deserializeError, ErrorObject} from './index.js';
+import {expectType, expectAssignable} from 'tsd';
+import {serializeError, deserializeError, ErrorObject, Options} from './index.js';
 
 const error = new Error('unicorn');
 
 expectType<number>(serializeError(1));
 expectType<ErrorObject>(serializeError(error));
+expectAssignable<Options>({maxDepth: 1});
 
 expectType<Error>(deserializeError({
 	message: 'error message',
diff --git a/test.js b/test.js
index 38cd5a7..c1d8111 100644
--- a/test.js
+++ b/test.js
@@ -209,6 +209,41 @@ test('deserialized name, stack and message should not be enumerable, other props
 	}
 });
 
+test('should deserialize properties up to `Options.maxDepth` levels deep', t => {
+	const error = new Error('errorMessage');
+	const object = {
+		message: error.message,
+		name: error.name,
+		stack: error.stack,
+		one: {
+			two: {
+				three: {}
+			}
+		}
+	};
+
+	const levelZero = deserializeError(object, {maxDepth: 0});
+	const emptyError = new Error('test');
+	emptyError.message = '';
+	t.is(levelZero instanceof Error, true);
+	t.deepEqual(levelZero, emptyError);
+
+	const levelOne = deserializeError(object, {maxDepth: 1});
+	error.one = {};
+	t.is(levelOne instanceof Error, true);
+	t.deepEqual(levelOne, error);
+
+	const levelTwo = deserializeError(object, {maxDepth: 2});
+	error.one = {two: {}};
+	t.is(levelTwo instanceof Error, true);
+	t.deepEqual(levelTwo, error);
+
+	const levelThree = deserializeError(object, {maxDepth: 3});
+	error.one = {two: {three: {}}};
+	t.is(levelThree instanceof Error, true);
+	t.deepEqual(levelThree, error);
+});
+
 test('should serialize Date as ISO string', t => {
 	const date = {date: new Date(0)};
 	const serialized = serializeError(date);
@@ -290,3 +325,21 @@ test('should serialize custom error with `.toJSON` defined with `serializeError`
 	});
 	t.not(stack, undefined);
 });
+
+test('should serialize properties up to `Options.maxDepth` levels deep', t => {
+	const error = new Error('errorMessage');
+	error.one = {two: {three: {}}};
+	const {message, name, stack} = error;
+
+	const levelZero = serializeError(error, {maxDepth: 0});
+	t.deepEqual(levelZero, {});
+
+	const levelOne = serializeError(error, {maxDepth: 1});
+	t.deepEqual(levelOne, {message, name, stack, one: {}});
+
+	const levelTwo = serializeError(error, {maxDepth: 2});
+	t.deepEqual(levelTwo, {message, name, stack, one: {two: {}}});
+
+	const levelThree = serializeError(error, {maxDepth: 3});
+	t.deepEqual(levelThree, {message, name, stack, one: {two: {three: {}}}});
+});