Repository: sindresorhus/serialize-error
Pull request #104 (2025-01-04 09:35:53)

Avoid breaking with bad custom constructors
- Blocked by and based on https://github.com/sindresorhus/serialize-error/pull/102
- Also fixes https://github.com/sindresorhus/serialize-error/issues/78

Breaking change.

As the readme suggested, adding bad constructors to this map would break the entire application. Now it fails exactly when they're added.




Patch:
diff --git a/error-constructors.d.ts b/error-constructors.d.ts
index b3e8017..ec487a7 100644
--- a/error-constructors.d.ts
+++ b/error-constructors.d.ts
@@ -1,8 +1,10 @@
 /**
-Map of error constructors to recreate from the serialize `name` property. If the name is not found in this map, the errors will be deserialized as simple `Error` instances.
+Let `serialize-error` know about your custom error constructors so that when `{name: 'MyCustomError', message: 'It broke'}` is found, it uses the right error constructor. If "MyCustomError" isn't found in the global list of known constructors, it defaults to the base `Error` error constructor.
 
-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: The constructor must work without any arguments or this function will throw.
 */
-declare const errorConstructors: Map<string, ErrorConstructor>;
 
-export default errorConstructors;
+type BaseErrorConstructor = new (message?: string, ...arguments_: unknown[]) => Error;
+declare function addKnownErrorConstructor(constructor: BaseErrorConstructor): void;
+
+export {addKnownErrorConstructor};
diff --git a/error-constructors.js b/error-constructors.js
index bdad603..82a43d4 100644
--- a/error-constructors.js
+++ b/error-constructors.js
@@ -1,5 +1,6 @@
 const list = [
 	// Native ES errors https://262.ecma-international.org/12.0/#sec-well-known-intrinsic-objects
+	Error,
 	EvalError,
 	RangeError,
 	ReferenceError,
@@ -21,6 +22,20 @@ const list = [
 		constructor => [constructor.name, constructor],
 	);
 
-const errorConstructors = new Map(list);
+export const errorConstructors = new Map(list);
 
-export default errorConstructors;
+export function addKnownErrorConstructor(constructor) {
+	const {name} = constructor;
+	if (errorConstructors.has(name)) {
+		throw new Error(`The error constructor "${name}" is already known.`);
+	}
+
+	try {
+		// eslint-disable-next-line no-new -- It just needs to be verified
+		new constructor();
+	} catch (error) {
+		throw new Error(`The error constructor "${name}" is not compatible`, {cause: error});
+	}
+
+	errorConstructors.set(name, constructor);
+}
diff --git a/index.d.ts b/index.d.ts
index 5fa6689..dbee88d 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -1,6 +1,6 @@
 import {type Primitive, type JsonObject} from 'type-fest';
 
-export {default as errorConstructors} from './error-constructors.js';
+export {addKnownErrorConstructor} from './error-constructors.js';
 
 export type ErrorObject = {
 	name?: string;
diff --git a/index.js b/index.js
index d32d91a..ccc6a5e 100644
--- a/index.js
+++ b/index.js
@@ -1,4 +1,4 @@
-import errorConstructors from './error-constructors.js';
+import {errorConstructors} from './error-constructors.js';
 
 export class NonError extends Error {
 	name = 'NonError';
@@ -209,4 +209,4 @@ function isMinimumViableSerializedError(value) {
 	&& !Array.isArray(value);
 }
 
-export {default as errorConstructors} from './error-constructors.js';
+export {addKnownErrorConstructor} from './error-constructors.js';
diff --git a/readme.md b/readme.md
index e574422..0943988 100644
--- a/readme.md
+++ b/readme.md
@@ -58,13 +58,13 @@ console.log(unknown);
 The [list of known errors](./error-constructors.js) can be extended globally. This also works if `serialize-error` is a sub-dependency that's not used directly.
 
 ```js
-import {errorConstructors} from 'serialize-error';
+import {addKnownErrorConstructor} from 'serialize-error';
 import {MyCustomError} from './errors.js'
 
-errorConstructors.set('MyCustomError', MyCustomError)
+addKnownErrorConstructor(MyCustomError);
 ```
 
-**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.
+**Warning:** The constructor must work without any arguments or this function will throw.
 
 ## API
 


Test patch:
diff --git a/index.test-d.ts b/index.test-d.ts
index f3a8767..50202d9 100644
--- a/index.test-d.ts
+++ b/index.test-d.ts
@@ -2,6 +2,7 @@ import {expectType, expectAssignable} from 'tsd';
 import {
 	serializeError,
 	deserializeError,
+	addKnownErrorConstructor,
 	type ErrorObject,
 	type Options,
 } from './index.js';
@@ -18,3 +19,8 @@ expectType<Error>(deserializeError({
 	name: 'name',
 	code: 'code',
 }));
+
+addKnownErrorConstructor(Error);
+
+class CustomError extends Error {}
+addKnownErrorConstructor(CustomError);
diff --git a/test.js b/test.js
index 5f31174..3d413d5 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 errorConstructors from './error-constructors.js';
+import {errorConstructors, addKnownErrorConstructor} from './error-constructors.js';
 import {
 	serializeError,
 	deserializeError,
@@ -230,6 +230,19 @@ for (const [name, CustomError] of errorConstructors) {
 	});
 }
 
+test('should not allow adding incompatible or redundant error constructors', t => {
+	t.throws(() => {
+		addKnownErrorConstructor(Error);
+	}, {message: 'The error constructor "Error" is already known.'});
+	t.throws(() => {
+		addKnownErrorConstructor(class BadError {
+			constructor() {
+				throw new Error('The number you have dialed is not in service');
+			}
+		});
+	}, {message: 'The error constructor "BadError" is not compatible'});
+});
+
 test('should deserialize plain object', t => {
 	const object = {
 		message: 'error message',