Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@ export type Options = {
/**
Serialize an `Error` object into a plain object.

- Non-error values are passed through.
- Custom properties are preserved.
- Non-enumerable properties are kept non-enumerable (name, message, stack).
- Enumerable properties are kept enumerable (all properties besides the non-enumerable ones).
- Primitive values (including `null`, `undefined`, strings, numbers, etc.) and functions are wrapped in a `NonError` error and serialized.
- Buffer properties are replaced with `[object Buffer]`.
- Circular references are handled.
- If the input object has a `.toJSON()` method, then it's called instead of serializing the object's properties.
Expand Down Expand Up @@ -105,11 +107,7 @@ serializeError(error);
// => {horn: 'x', name, message, stack}
```
*/
export function serializeError<ErrorType>(error: ErrorType, options?: Options): ErrorType extends Primitive
? ErrorType
: unknown extends ErrorType
? unknown
: ErrorObject;
export function serializeError(error: unknown, options?: Options): ErrorObject;

/**
Deserialize a plain object or any value into an `Error` object.
Expand Down Expand Up @@ -159,7 +157,7 @@ isErrorLike({
isErrorLike(new Error('🦄'));
//=> true

isErrorLike(serializeError(new Error('🦄'));
isErrorLike(serializeError(new Error('🦄')));
//=> true

isErrorLike({
Expand Down
21 changes: 16 additions & 5 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ export class NonError extends Error {
}

static _prepareSuperMessage(message) {
// Handle BigInt specially to show the 'n' suffix
if (typeof message === 'bigint') {
return `${message}n`;
}

try {
return JSON.stringify(message);
return JSON.stringify(message) ?? String(message);
} catch {
return String(message);
}
Expand Down Expand Up @@ -177,12 +182,18 @@ export function serializeError(value, options = {}) {

// People sometimes throw things besides Error objects…
if (typeof value === 'function') {
// `JSON.stringify()` discards functions. We do too, unless a function is thrown directly.
// We intentionally use `||` because `.name` is an empty string for anonymous functions.
return `[Function: ${value.name || 'anonymous'}]`;
value = '<Function>';
}

return value;
return destroyCircular({
from: new NonError(value),
seen: [],
forceEnumerable: true,
maxDepth,
depth: 0,
useToJSON,
serialize: true,
});
}

export function deserializeError(value, options = {}) {
Expand Down
10 changes: 8 additions & 2 deletions index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@ import {

const error = new Error('unicorn');

expectTypeOf(serializeError(1)).toEqualTypeOf<number>();
expectTypeOf(serializeError(error as unknown)).toEqualTypeOf<unknown>();
expectTypeOf(serializeError(1)).toEqualTypeOf<ErrorObject>();
expectTypeOf(serializeError('hello')).toEqualTypeOf<ErrorObject>();
expectTypeOf(serializeError(true)).toEqualTypeOf<ErrorObject>();
expectTypeOf(serializeError(undefined)).toEqualTypeOf<ErrorObject>();
expectTypeOf(serializeError(null)).toEqualTypeOf<ErrorObject>();
// eslint-disable-next-line @typescript-eslint/no-empty-function
expectTypeOf(serializeError(() => {})).toEqualTypeOf<ErrorObject>();
expectTypeOf(serializeError(error as unknown)).toEqualTypeOf<ErrorObject>();
expectTypeOf(serializeError(error)).toEqualTypeOf<ErrorObject>();
expectTypeOf({maxDepth: 1}).toMatchTypeOf<Options>();

Expand Down
4 changes: 2 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,10 @@ addKnownErrorConstructor(MyCustomError);

Serialize an `Error` object into a plain object.

- Non-error values are passed through.
- Custom properties are preserved.
- Non-enumerable properties are kept non-enumerable (name, message, stack).
- Enumerable properties are kept enumerable (all properties besides the non-enumerable ones).
- Primitive values (including `null`, `undefined`, strings, numbers, etc.) and functions are wrapped in a `NonError` error and serialized.
- Buffer properties are replaced with `[object Buffer]`.
- Circular references are handled.
- If the input object has a `.toJSON()` method, then it's called instead of serializing the object's properties.
Expand Down Expand Up @@ -186,7 +186,7 @@ isErrorLike({
isErrorLike(new Error('🦄'));
//=> true

isErrorLike(serializeError(new Error('🦄'));
isErrorLike(serializeError(new Error('🦄')));
//=> true

isErrorLike({
Expand Down
60 changes: 49 additions & 11 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,15 +98,6 @@ test('should discard streams', t => {
t.deepEqual(serializeError({s: new Stream.PassThrough()}), {s: '[object Stream]'}, 'Stream.PassThrough');
});

test('should replace top-level functions with a helpful string', t => {
function a() {}
function b() {}
a.b = b;

const serialized = serializeError(a);
t.is(serialized, '[Function: a]');
});

test('should drop functions', t => {
function a() {}
a.foo = 'bar;';
Expand Down Expand Up @@ -179,9 +170,56 @@ test('should serialize AggregateError', t => {
t.false(serialized.errors[0] instanceof Error);
});

test('should handle top-level null values', t => {
test('should serialize undefined to NonError', t => {
const serialized = serializeError(undefined);
t.is(serialized.name, 'NonError');
t.is(serialized.message, 'undefined');
t.truthy(serialized.stack);
});

test('should serialize values to NonError', t => {
// String
const stringResult = serializeError('hello');
t.is(stringResult.name, 'NonError');
t.is(stringResult.message, '"hello"');
t.truthy(stringResult.stack);

// Number
const numberResult = serializeError(42);
t.is(numberResult.name, 'NonError');
t.is(numberResult.message, '42');
t.truthy(numberResult.stack);

// Boolean
const booleanResult = serializeError(true);
t.is(booleanResult.name, 'NonError');
t.is(booleanResult.message, 'true');
t.truthy(booleanResult.stack);

// Symbol
const symbolResult = serializeError(Symbol('test'));
t.is(symbolResult.name, 'NonError');
t.is(symbolResult.message, 'Symbol(test)');
t.truthy(symbolResult.stack);

// BigInt
const bigIntResult = serializeError(BigInt(123));
t.is(bigIntResult.name, 'NonError');
t.is(bigIntResult.message, '123n');
t.truthy(bigIntResult.stack);

// Function
const functionResult = serializeError(() => {});
t.is(functionResult.name, 'NonError');
t.is(functionResult.message, '"<Function>"');
t.truthy(functionResult.stack);
});

test('should serialize null to NonError', t => {
const serialized = serializeError(null);
t.is(serialized, null);
t.is(serialized.name, 'NonError');
t.is(serialized.message, 'null');
t.truthy(serialized.stack);
});

test('should deserialize null', t => {
Expand Down