Skip to content

Commit

Permalink
Preserve error constructor (#70)
Browse files Browse the repository at this point in the history
  • Loading branch information
fregante committed Apr 18, 2022
1 parent bdf7ad4 commit 49db63a
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 12 deletions.
8 changes: 8 additions & 0 deletions error-constructors.d.ts
@@ -0,0 +1,8 @@
/**
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.
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.
*/
declare const errorConstructors: Map<string, ErrorConstructor>;

export default errorConstructors;
26 changes: 26 additions & 0 deletions error-constructors.js
@@ -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,

// 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
@@ -1,5 +1,7 @@
import {Primitive, JsonObject} from 'type-fest';

export {default as errorConstructors} from './error-constructors.js';

export type ErrorObject = {
name?: string;
message?: string;
Expand Down
31 changes: 20 additions & 11 deletions index.js
@@ -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;

// 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
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
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
@@ -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

0 comments on commit 49db63a

Please sign in to comment.