Repository: sindresorhus/serialize-error
Pull request #77 (2022-05-09 04:22:13)

Expect a `message` property at a minimum to be interpreted as an Error

sindresorhus: I think the docs should explicitly mention what we consider an error when deserializing: https://github.com/sindresorhus/serialize-error#deserializeerrorvalue-options
fregante: By the way this is another breaking change, technically, because `{name: "Error"}` would be deserialized as `new Error('')` before while now it's a `new NonError('{"name": "Error"}')`
sindresorhus: Can you fix the merge conflict?

Patch:
diff --git a/index.d.ts b/index.d.ts
index dff06e7..dc47c85 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -113,11 +113,13 @@ export function serializeError<ErrorType>(error: ErrorType, options?: Options):
 Deserialize a plain object or any value into an `Error` object.
 
 - `Error` objects are passed through.
-- Non-error values are wrapped in a `NonError` error.
+- Objects that have at least a `message` property are interpreted as errors.
+- All other values are wrapped in a `NonError` error.
 - Custom properties are preserved.
 - Non-enumerable properties are kept non-enumerable (name, message, stack, cause).
 - Enumerable properties are kept enumerable (all properties besides the non-enumerable ones).
 - Circular references are handled.
+- Native error constructors are preserved (TypeError, DOMException, etc) and more can be added.
 
 @example
 ```
diff --git a/index.js b/index.js
index 9e5e7b3..dd98dce 100644
--- a/index.js
+++ b/index.js
@@ -172,7 +172,7 @@ export function deserializeError(value, options = {}) {
 		return value;
 	}
 
-	if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
+	if (isMinimumViableSerializedError(value)) {
 		const Error = getErrorConstructor(value.name);
 		return destroyCircular({
 			from: value,
@@ -188,11 +188,18 @@ export function deserializeError(value, options = {}) {
 }
 
 export function isErrorLike(value) {
-	return value
+	return Boolean(value)
 	&& typeof value === 'object'
 	&& 'name' in value
 	&& 'message' in value
 	&& 'stack' in value;
 }
 
+function isMinimumViableSerializedError(value) {
+	return Boolean(value)
+	&& typeof value === 'object'
+	&& 'message' in value
+	&& !Array.isArray(value);
+}
+
 export {default as errorConstructors} from './error-constructors.js';
diff --git a/readme.md b/readme.md
index 8fe02de..e574422 100644
--- a/readme.md
+++ b/readme.md
@@ -64,14 +64,12 @@ import {MyCustomError} from './errors.js'
 errorConstructors.set('MyCustomError', MyCustomError)
 ```
 
-**Warning:** Only simple and standard error constructors are supported, like `new MyCustomError(name)`. If your error constructor **requires** a second parameter or does not accept a string as first parameter, adding it to this map **will** break the deserialization.
+**Warning:** Only simple and standard error constructors are supported, like `new MyCustomError(message)`. If your error constructor **requires** a second parameter or does not accept a string as first parameter, adding it to this map **will** break the deserialization.
 
 ## API
 
 ### serializeError(value, options?)
 
-Type: `Error | unknown`
-
 Serialize an `Error` object into a plain object.
 
 - Non-error values are passed through.
@@ -83,7 +81,11 @@ Serialize an `Error` object into a plain object.
 - If the input object has a `.toJSON()` method, then it's called instead of serializing the object's properties.
 - It's up to `.toJSON()` implementation to handle circular references and enumerability of the properties.
 
-`.toJSON` examples:
+### value
+
+Type: `Error | unknown`
+
+### toJSON implementation examples
 
 ```js
 import {serializeError} from 'serialize-error';
@@ -118,18 +120,21 @@ serializeError(error);
 
 ### deserializeError(value, options?)
 
-Type: `{[key: string]: unknown} | unknown`
-
 Deserialize a plain object or any value into an `Error` object.
 
 - `Error` objects are passed through.
-- Non-error values are wrapped in a `NonError` error.
+- Objects that have at least a `message` property are interpreted as errors.
+- All other values are wrapped in a `NonError` error.
 - Custom properties are preserved.
 - Non-enumerable properties are kept non-enumerable (name, message, stack, cause).
 - Enumerable properties are kept enumerable (all properties besides the non-enumerable ones).
 - Circular references are handled.
 - [Native error constructors](./error-constructors.js) are preserved (TypeError, DOMException, etc) and [more can be added.](#error-constructors)
 
+### value
+
+Type: `{message: string} | unknown`
+
 ### options
 
 Type: `object`


Test patch:
diff --git a/test.js b/test.js
index 7b46b8f..34eb7f0 100644
--- a/test.js
+++ b/test.js
@@ -192,10 +192,23 @@ test('should deserialize array', t => {
 	deserializeNonError(t, [1]);
 });
 
+test('should deserialize empty object', t => {
+	deserializeNonError(t, {});
+});
+
+test('should ignore Error instance', t => {
+	const originalError = new Error('test');
+	const deserialized = deserializeError(originalError);
+	t.is(deserialized, originalError);
+});
+
 test('should deserialize error', t => {
-	const deserialized = deserializeError(new Error('test'));
+	const deserialized = deserializeError({
+		message: 'Stuff happened',
+	});
 	t.true(deserialized instanceof Error);
-	t.is(deserialized.message, 'test');
+	t.is(deserialized.name, 'Error');
+	t.is(deserialized.message, 'Stuff happened');
 });
 
 test('should deserialize and preserve existing properties', t => {
@@ -247,7 +260,13 @@ for (const property of ['cause', 'any']) {
 				message: 'source error message',
 				stack: 'at <anonymous>:3:14',
 				name: 'name',
-				code: 'code',
+				code: 'the apple',
+				[property]: {
+					message: 'original error message',
+					stack: 'at <anonymous>:16:9',
+					name: 'name',
+					code: 'the snake',
+				},
 			},
 		};
 
@@ -256,7 +275,14 @@ for (const property of ['cause', 'any']) {
 		t.is(nested.message, 'source error message');
 		t.is(nested.stack, 'at <anonymous>:3:14');
 		t.is(nested.name, 'name');
-		t.is(nested.code, 'code');
+		t.is(nested.code, 'the apple');
+
+		const {[property]: deepNested} = nested;
+		t.true(deepNested instanceof Error);
+		t.is(deepNested.message, 'original error message');
+		t.is(deepNested.stack, 'at <anonymous>:16:9');
+		t.is(deepNested.name, 'name');
+		t.is(deepNested.code, 'the snake');
 	});
 }