Skip to content

Commit

Permalink
Avoid breaking with bad custom constructors (#104)
Browse files Browse the repository at this point in the history
  • Loading branch information
fregante authored Jan 4, 2025
1 parent 7fc2898 commit e593d37
Show file tree
Hide file tree
Showing 7 changed files with 49 additions and 13 deletions.
10 changes: 6 additions & 4 deletions error-constructors.d.ts
Original file line number Diff line number Diff line change
@@ -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};
19 changes: 17 additions & 2 deletions error-constructors.js
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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);
}
2 changes: 1 addition & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
4 changes: 2 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import errorConstructors from './error-constructors.js';
import {errorConstructors} from './error-constructors.js';

export class NonError extends Error {
name = 'NonError';
Expand Down Expand Up @@ -209,4 +209,4 @@ function isMinimumViableSerializedError(value) {
&& !Array.isArray(value);
}

export {default as errorConstructors} from './error-constructors.js';
export {addKnownErrorConstructor} from './error-constructors.js';
6 changes: 6 additions & 0 deletions index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {expectType, expectAssignable} from 'tsd';
import {
serializeError,
deserializeError,
addKnownErrorConstructor,
type ErrorObject,
type Options,
} from './index.js';
Expand All @@ -18,3 +19,8 @@ expectType<Error>(deserializeError({
name: 'name',
code: 'code',
}));

addKnownErrorConstructor(Error);

class CustomError extends Error {}
addKnownErrorConstructor(CustomError);
6 changes: 3 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
15 changes: 14 additions & 1 deletion test.js
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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',
Expand Down

0 comments on commit e593d37

Please sign in to comment.