Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preserve error constructor #70

Merged
merged 17 commits into from
Apr 18, 2022
Merged
6 changes: 6 additions & 0 deletions error-constructors.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
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.
*/
declare const errorConstructors: Map<string, ErrorConstructor>;

export default errorConstructors;
26 changes: 26 additions & 0 deletions error-constructors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const list = [
// Native ES errors https://262.ecma-international.org/12.0/#sec-well-known-intrinsic-objects
EvalError,
RangeError,
ReferenceError,
SyntaxError,
TypeError,
URIError,

// Built-in errors
globalThis.DOMException,
sindresorhus marked this conversation as resolved.
Show resolved Hide resolved

// Node-specific errors
// https://nodejs.org/api/errors.html
globalThis.AssertionError,
globalThis.SystemError,
]
// Non-native Errors are used with `globalThis` because they might be missing. This filter drops them when undefined.
.filter(Boolean)
.map(
constructor => [constructor.name, constructor],
);

const errorConstructors = new Map(list);

export default errorConstructors;
2 changes: 2 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {Primitive, JsonObject} from 'type-fest';

export {default as errorConstructors} from './error-constructors.js';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds support for custom error constructors to be added. This only supports simple errors so I mentioned this in the docs and in the types. A new issue can be created to add support for such error constructors.

Also, instead of adding error-constructors.js to the exports map, I just added another export here. It's cleaner anyway.


export type ErrorObject = {
name?: string;
message?: string;
Expand Down
31 changes: 20 additions & 11 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import errorConstructors from './error-constructors.js';

export class NonError extends Error {
name = 'NonError';

Expand Down Expand Up @@ -46,6 +48,8 @@ const toJSON = from => {
return json;
};

const getErrorConstructor = name => errorConstructors.get(name) || Error;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried ?? but it fails in Node 12. If desired, Node 12 support can probably be dropped separately so it appears in the changelog


// eslint-disable-next-line complexity
const destroyCircular = ({
from,
Expand All @@ -68,16 +72,19 @@ const destroyCircular = ({
return toJSON(from);
}

const destroyLocal = value => destroyCircular({
from: value,
seen: [...seen],
// eslint-disable-next-line unicorn/error-message
to_: isErrorLike(value) ? new Error() : undefined,
forceEnumerable,
maxDepth,
depth,
useToJSON,
});
const destroyLocal = value => {
const Error = getErrorConstructor(value.name);
return destroyCircular({
from: value,
seen: [...seen],

to_: isErrorLike(value) ? new Error() : undefined,
forceEnumerable,
maxDepth,
depth,
useToJSON,
});
};

for (const [key, value] of Object.entries(from)) {
// eslint-disable-next-line node/prefer-global/buffer
Expand Down Expand Up @@ -159,10 +166,10 @@ export function deserializeError(value, options = {}) {
}

if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
const Error = getErrorConstructor(value.name);
return destroyCircular({
from: value,
seen: [],
// eslint-disable-next-line unicorn/error-message
to_: new Error(),
maxDepth,
depth: 0,
Expand All @@ -179,3 +186,5 @@ export function isErrorLike(value) {
&& 'message' in value
&& 'stack' in value;
}

export {errorConstructors};
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
},
"files": [
"index.js",
"index.d.ts"
"index.d.ts",
"error-constructors.js",
"error-constructors.d.ts"
],
"keywords": [
"error",
Expand Down
36 changes: 36 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,41 @@ console.log(deserialized);
//=> [Error: 🦄]
```

### Error constructors

When a serialized error with a known `name` is encountered, it will be deserialized using the corresponding error constructor, while enknown error names will be deserialized as regular errors:

```js
import {deserializeError} from 'serialize-error';

const known = deserializeError({
name: 'TypeError',
message: '🦄'
});

console.log(known);
//=> [TypeError: 🦄] <-- still a TypeError

const unknown = deserializeError({
name: 'TooManyCooksError',
message: '🦄'
});

console.log(unknown);
//=> [Error: 🦄] <-- just a regular Error
```

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 {MyCustomError} from './errors.js'

errorConstructors.set('MyCustomError', MyCustomError)
```

**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.

## API

### serializeError(value, options?)
Expand Down Expand Up @@ -93,6 +128,7 @@ Deserialize a plain object or any value into an `Error` object.
- Non-enumerable properties are kept non-enumerable (name, message, stack, cause).
- Enumerable properties are kept enumerable (all properties besides the non-enumerable ones).
- Circular references are handled.
- [Native error constructors](./error-constructors.js) are preserved (TypeError, DOMException, etc) and [more can be added.](#error-constructors)

### options

Expand Down
12 changes: 12 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {Buffer} from 'node:buffer';
import Stream from 'node:stream';
import test from 'ava';
import errorConstructors from './error-constructors.js';
import {serializeError, deserializeError, isErrorLike} from './index.js';

function deserializeNonError(t, value) {
Expand Down Expand Up @@ -184,6 +185,17 @@ test('should deserialize and preserve existing properties', t => {
t.true(deserialized.customProperty);
});

for (const [name, CustomError] of errorConstructors) {
test(`should deserialize and preserve the ${name} constructor`, t => {
const deserialized = deserializeError({
name,
message: 'foo',
});
t.true(deserialized instanceof CustomError);
t.is(deserialized.message, 'foo');
});
}

test('should deserialize plain object', t => {
const object = {
message: 'error message',
Expand Down