Repository: sindresorhus/serialize-error (js)

Issue:
Feature request: handle AggregateErrors
Currently, as far as I can tell, AggregateErrors get serialized like this, no matter what errors they contain:

```
{
  name: 'AggregateError',
  message: '',
  stack: 'AggregateError\n' +
    '    at REPL4:1:24\n' +
    '    at Script.runInThisContext (node:vm:129:12)\n' +
    '    at REPLServer.defaultEval (node:repl:572:29)\n' +
    '    at bound (node:domain:433:15)\n' +
    '    at REPLServer.runBound [as eval] (node:domain:444:12)\n' +
    '    at REPLServer.onLine (node:repl:902:10)\n' +
    '    at REPLServer.emit (node:events:525:35)\n' +
    '    at REPLServer.emit (node:domain:489:12)\n' +
    '    at [_onLine] [as _onLine] (node:internal/readline/interface:422:12)\n' +
    '    at [_line] [as _line] (node:internal/readline/interface:892:18)'
}
```

(This is with Node 18.14.2, but I don't think anything materially different will happen on a different runtime.)

Can the `errors` array get serialized nicely?

Pull request description:
Add support for `AggregateError`
- Blocked by and based on https://github.com/sindresorhus/serialize-error/pull/102
- Closes #91 

Patch:
diff --git a/error-constructors.js b/error-constructors.js
index 82a43d4..99938f0 100644
--- a/error-constructors.js
+++ b/error-constructors.js
@@ -7,6 +7,7 @@ const list = [
 	SyntaxError,
 	TypeError,
 	URIError,
+	AggregateError,
 
 	// Built-in errors
 	globalThis.DOMException,
diff --git a/index.d.ts b/index.d.ts
index dbee88d..d1b319e 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -107,7 +107,9 @@ serializeError(error);
 */
 export function serializeError<ErrorType>(error: ErrorType, options?: Options): ErrorType extends Primitive
 	? ErrorType
-	: ErrorObject;
+	: unknown extends ErrorType
+		? unknown
+		: ErrorObject;
 
 /**
 Deserialize a plain object or any value into an `Error` object.
diff --git a/index.js b/index.js
index ccc6a5e..29dd979 100644
--- a/index.js
+++ b/index.js
@@ -37,6 +37,10 @@ const errorProperties = [
 		property: 'cause',
 		enumerable: false,
 	},
+	{
+		property: 'errors',
+		enumerable: false,
+	},
 ];
 
 const toJsonWasCalled = new WeakSet();
@@ -48,7 +52,12 @@ const toJSON = from => {
 	return json;
 };
 
-const getErrorConstructor = name => errorConstructors.get(name) ?? Error;
+const newError = name => {
+	const ErrorConstructor = errorConstructors.get(name) ?? Error;
+	return ErrorConstructor === AggregateError
+		? new ErrorConstructor([])
+		: new ErrorConstructor();
+};
 
 // eslint-disable-next-line complexity
 const destroyCircular = ({
@@ -65,8 +74,7 @@ const destroyCircular = ({
 		if (Array.isArray(from)) {
 			to = [];
 		} else if (!serialize && isErrorLike(from)) {
-			const Error = getErrorConstructor(from.name);
-			to = new Error();
+			to = newError(from.name);
 		} else {
 			to = {};
 		}
@@ -131,7 +139,9 @@ const destroyCircular = ({
 		for (const {property, enumerable} of errorProperties) {
 			if (from[property] !== undefined && from[property] !== null) {
 				Object.defineProperty(to, property, {
-					value: isErrorLike(from[property]) ? continueDestroyCircular(from[property]) : from[property],
+					value: isErrorLike(from[property]) || Array.isArray(from[property])
+						? continueDestroyCircular(from[property])
+						: from[property],
 					enumerable: forceEnumerable ? true : enumerable,
 					configurable: true,
 					writable: true,
@@ -179,11 +189,10 @@ export function deserializeError(value, options = {}) {
 	}
 
 	if (isMinimumViableSerializedError(value)) {
-		const Error = getErrorConstructor(value.name);
 		return destroyCircular({
 			from: value,
 			seen: [],
-			to: new Error(),
+			to: newError(value.name),
 			maxDepth,
 			depth: 0,
 			serialize: false,
diff --git a/package.json b/package.json
index ae76c4f..808dcb7 100644
--- a/package.json
+++ b/package.json
@@ -41,6 +41,7 @@
 	},
 	"devDependencies": {
 		"ava": "^6.2.0",
+		"expect-type": "^1.1.0",
 		"tsd": "^0.31.2",
 		"xo": "^0.60.0"
 	}


Test patch:
diff --git a/index.test-d.ts b/index.test-d.ts
index 50202d9..bc7c9a8 100644
--- a/index.test-d.ts
+++ b/index.test-d.ts
@@ -1,4 +1,4 @@
-import {expectType, expectAssignable} from 'tsd';
+import {expectTypeOf} from 'expect-type';
 import {
 	serializeError,
 	deserializeError,
@@ -9,16 +9,17 @@ import {
 
 const error = new Error('unicorn');
 
-expectType<number>(serializeError(1));
-expectType<ErrorObject>(serializeError(error));
-expectAssignable<Options>({maxDepth: 1});
+expectTypeOf(serializeError(1)).toEqualTypeOf<number>();
+expectTypeOf(serializeError(error as unknown)).toEqualTypeOf<unknown>();
+expectTypeOf(serializeError(error)).toEqualTypeOf<ErrorObject>();
+expectTypeOf({maxDepth: 1}).toMatchTypeOf<Options>();
 
-expectType<Error>(deserializeError({
+expectTypeOf(deserializeError({
 	message: 'error message',
 	stack: 'at <anonymous>:1:13',
 	name: 'name',
 	code: 'code',
-}));
+})).toEqualTypeOf<Error>();
 
 addKnownErrorConstructor(Error);
 
diff --git a/test.js b/test.js
index 3d413d5..305de49 100644
--- a/test.js
+++ b/test.js
@@ -165,6 +165,20 @@ test('should serialize the cause property', t => {
 	t.false(serialized.cause.cause instanceof Error);
 });
 
+test('should serialize AggregateError', t => {
+	// eslint-disable-next-line unicorn/error-message -- Testing this eventuality
+	const error = new AggregateError([new Error('inner error')]);
+
+	const serialized = serializeError(error);
+	t.is(serialized.message, ''); // Default error message
+	t.true(Array.isArray(serialized.errors));
+	t.like(serialized.errors[0], {
+		name: 'Error',
+		message: 'inner error',
+	});
+	t.false(serialized.errors[0] instanceof Error);
+});
+
 test('should handle top-level null values', t => {
 	const serialized = serializeError(null);
 	t.is(serialized, null);
@@ -369,6 +383,21 @@ test('should deserialize properties up to `Options.maxDepth` levels deep', t =>
 	t.deepEqual(levelThree, error);
 });
 
+test('should deserialize AggregateError', t => {
+	const deserialized = deserializeError({
+		name: 'AggregateError',
+		message: '',
+		errors: [
+			{name: 'Error', message: 'inner error', stack: ''},
+		],
+	});
+	t.true(deserialized instanceof AggregateError);
+	t.is(deserialized.message, '');
+	t.true(Array.isArray(deserialized.errors));
+	t.is(deserialized.errors[0].message, 'inner error');
+	t.true(deserialized.errors[0] instanceof Error);
+});
+
 test('should ignore invalid error-like objects', t => {
 	const errorLike = {
 		name: 'Error',