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; -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/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(deserializeError({ name: 'name', code: 'code', })); + +addKnownErrorConstructor(Error); + +class CustomError extends Error {} +addKnownErrorConstructor(CustomError); 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 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',