Repository: sindresorhus/serialize-error (js)

Issue:
Add `isErrorObject` type guard / function
What do you think about exporting a type-checker function (like `Array.isArray`) that doubles as a type guard?

```ts
export function isErrorObject(error: unknown): error is ErrorObject {
  return error && typeof error === 'object' && 'message' in error;
}
```



Pull request description:
Add `isErrorLike()` method
Closes #47 

Since `deserializeError` also accepts `Error` instances, I decided to do the same for this detector.

Names:

- isErrorLike: it matches the "array-like" nomenclature, which also is `true` or real arrays
- isSerializedError: can't use it because the function also matches regular `Error` instances
- isErrorObject: it matches the `ErrorObject` type, but it reads like "is `Error` object", as in "is `Error` instance. 

Unrelated suggestions:
- rename the `ErrorObject` type to `ErrorLike`
- make `name`, `message` and `stack` properties of this type mandatory, because they always exist in `new Error()` and its serialized counterpart 

Patch:
diff --git a/index.d.ts b/index.d.ts
index 4111da3..1b987c7 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -5,8 +5,18 @@ export type ErrorObject = {
 	stack?: string;
 	message?: string;
 	code?: string;
+	cause?: unknown;
 } & JsonObject;
 
+export type ErrorLike = {
+	[key: string]: unknown;
+	name: string;
+	stack: string;
+	message: string;
+	code?: string;
+	cause?: unknown;
+};
+
 export interface Options {
 	/**
 	The maximum depth of properties to preserve when serializing/deserializing.
@@ -74,21 +84,16 @@ console.log(serializeError(error));
 ```
 import {serializeError} from 'serialize-error';
 
-class ErrorWithToJSON extends Error {
-	constructor() {
-		super('🦄');
-		this.date = new Date();
-	}
+const error = new Error('Unicorn');
 
+error.horn = {
 	toJSON() {
-		return serializeError(this);
+		return 'x';
 	}
-}
+};
 
-const error = new ErrorWithToJSON();
-
-console.log(serializeError(error));
-// => {date: '1970-01-01T00:00:00.000Z', message: '🦄', name, stack}
+serializeError(error);
+// => {horn: 'x', name, message, stack}
 ```
 */
 export function serializeError<ErrorType>(error: ErrorType, options?: Options): ErrorType extends Primitive
@@ -120,3 +125,37 @@ console.log(error);
 ```
 */
 export function deserializeError(errorObject: ErrorObject | unknown, options?: Options): Error;
+
+/**
+Predicate to determine whether a value looks like an error, even if it's not an instance of `Error`. It must have at least the `name`, `message`, and `stack` properties.
+
+@example
+```
+import {isErrorLike} from 'serialize-error';
+
+const error = new Error('🦄');
+error.one = {two: {three: {}}};
+
+isErrorLike({
+	name: 'DOMException',
+	message: 'It happened',
+	stack: 'at foo (index.js:2:9)',
+});
+//=> true
+
+isErrorLike(new Error('🦄'));
+//=> true
+
+isErrorLike(serializeError(new Error('🦄'));
+//=> true
+
+isErrorLike({
+	name: 'Bluberricious pancakes',
+	stack: 12,
+	ingredients: 'Blueberry',
+});
+//=> false
+```
+
+*/
+export function isErrorLike(value: unknown): value is ErrorLike;
diff --git a/index.js b/index.js
index 947b209..28b26f3 100644
--- a/index.js
+++ b/index.js
@@ -134,7 +134,7 @@ export function serializeError(value, options = {}) {
 	// People sometimes throw things besides Error objects…
 	if (typeof value === 'function') {
 		// `JSON.stringify()` discards functions. We do too, unless a function is thrown directly.
-		return `[Function: ${(value.name || 'anonymous')}]`;
+		return `[Function: ${value.name || 'anonymous'}]`;
 	}
 
 	return value;
@@ -161,3 +161,11 @@ export function deserializeError(value, options = {}) {
 
 	return new NonError(value);
 }
+
+export function isErrorLike(value) {
+	return value
+	&& typeof value === 'object'
+	&& 'name' in value
+	&& 'message' in value
+	&& 'stack' in value;
+}
diff --git a/readme.md b/readme.md
index 60634fc..4f88658 100644
--- a/readme.md
+++ b/readme.md
@@ -20,7 +20,7 @@ const error = new Error('🦄');
 console.log(error);
 //=> [Error: 🦄]
 
-const serialized = serializeError(error)
+const serialized = serializeError(error);
 
 console.log(serialized);
 //=> {name: 'Error', message: '🦄', stack: 'Error: 🦄\n    at Object.<anonymous> …'}
@@ -69,21 +69,16 @@ serializeError(error);
 ```js
 import {serializeError} from 'serialize-error';
 
-class ErrorWithToJSON extends Error {
-	constructor() {
-		super('🦄');
-		this.date = new Date();
-	}
+const error = new Error('Unicorn');
 
+error.horn = {
 	toJSON() {
-		return serializeError(this);
+		return 'x';
 	}
-}
+};
 
-const error = new ErrorWithToJSON();
-
-console.log(serializeError(error));
-// => {date: '1970-01-01T00:00:00.000Z', message: '🦄', name, stack}
+serializeError(error);
+// => {horn: 'x', name, message, stack}
 ```
 
 ### deserializeError(value, options?)
@@ -122,3 +117,34 @@ console.log(serializeError(error, {maxDepth: 1}));
 console.log(serializeError(error, {maxDepth: 2}));
 //=> {name: 'Error', message: '…', one: { two: {}}}
 ```
+
+### isErrorLike(value)
+
+Predicate to determine whether a value looks like an error, even if it's not an instance of `Error`. It must have at least the `name`, `message`, and `stack` properties.
+
+```js
+import {isErrorLike} from 'serialize-error';
+
+const error = new Error('🦄');
+error.one = {two: {three: {}}};
+
+isErrorLike({
+	name: 'DOMException',
+	message: 'It happened',
+	stack: 'at foo (index.js:2:9)',
+});
+//=> true
+
+isErrorLike(new Error('🦄'));
+//=> true
+
+isErrorLike(serializeError(new Error('🦄'));
+//=> true
+
+isErrorLike({
+	name: 'Bluberricious pancakes',
+	stack: 12,
+	ingredients: 'Blueberry',
+});
+//=> false
+```


Test patch:
diff --git a/test.js b/test.js
index 717efac..bfb24e3 100644
--- a/test.js
+++ b/test.js
@@ -1,7 +1,7 @@
 import {Buffer} from 'node:buffer';
 import Stream from 'node:stream';
 import test from 'ava';
-import {serializeError, deserializeError} from './index.js';
+import {serializeError, deserializeError, isErrorLike} from './index.js';
 
 function deserializeNonError(t, value) {
 	const deserialized = deserializeError(value);
@@ -383,3 +383,26 @@ test('should serialize properties up to `Options.maxDepth` levels deep', t => {
 	const levelThree = serializeError(error, {maxDepth: 3});
 	t.deepEqual(levelThree, {message, name, stack, one: {two: {three: {}}}});
 });
+
+test('should identify serialized errors', t => {
+	t.true(isErrorLike(serializeError(new Error('I’m missing more than just your body'))));
+	// eslint-disable-next-line unicorn/error-message -- Testing this eventuality
+	t.true(isErrorLike(serializeError(new Error())));
+	t.true(isErrorLike({
+		name: 'Error',
+		message: 'Is it too late now to say sorry',
+		stack: 'at <anonymous>:3:14',
+	}));
+
+	t.false(isErrorLike({
+		name: 'Bluberricious pancakes',
+		stack: 12,
+		ingredients: 'Blueberry',
+	}));
+
+	t.false(isErrorLike({
+		name: 'Edwin Monton',
+		message: 'We’ve been trying to reach you about your car’s extended warranty',
+		medium: 'Glass bottle in ocean',
+	}));
+});