diff --git a/index.d.ts b/index.d.ts index d1b319e..ee24588 100644 --- a/index.d.ts +++ b/index.d.ts @@ -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. @@ -105,11 +107,7 @@ serializeError(error); // => {horn: 'x', name, message, stack} ``` */ -export function serializeError(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. @@ -159,7 +157,7 @@ isErrorLike({ isErrorLike(new Error('🦄')); //=> true -isErrorLike(serializeError(new Error('🦄')); +isErrorLike(serializeError(new Error('🦄'))); //=> true isErrorLike({ diff --git a/index.js b/index.js index f1c050c..e45548c 100644 --- a/index.js +++ b/index.js @@ -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); } @@ -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 = ''; } - return value; + return destroyCircular({ + from: new NonError(value), + seen: [], + forceEnumerable: true, + maxDepth, + depth: 0, + useToJSON, + serialize: true, + }); } export function deserializeError(value, options = {}) { diff --git a/index.test-d.ts b/index.test-d.ts index bc7c9a8..9e41d84 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -9,8 +9,14 @@ import { const error = new Error('unicorn'); -expectTypeOf(serializeError(1)).toEqualTypeOf(); -expectTypeOf(serializeError(error as unknown)).toEqualTypeOf(); +expectTypeOf(serializeError(1)).toEqualTypeOf(); +expectTypeOf(serializeError('hello')).toEqualTypeOf(); +expectTypeOf(serializeError(true)).toEqualTypeOf(); +expectTypeOf(serializeError(undefined)).toEqualTypeOf(); +expectTypeOf(serializeError(null)).toEqualTypeOf(); +// eslint-disable-next-line @typescript-eslint/no-empty-function +expectTypeOf(serializeError(() => {})).toEqualTypeOf(); +expectTypeOf(serializeError(error as unknown)).toEqualTypeOf(); expectTypeOf(serializeError(error)).toEqualTypeOf(); expectTypeOf({maxDepth: 1}).toMatchTypeOf(); diff --git a/readme.md b/readme.md index 0943988..ab81eac 100644 --- a/readme.md +++ b/readme.md @@ -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. @@ -186,7 +186,7 @@ isErrorLike({ isErrorLike(new Error('🦄')); //=> true -isErrorLike(serializeError(new Error('🦄')); +isErrorLike(serializeError(new Error('🦄'))); //=> true isErrorLike({ diff --git a/test.js b/test.js index 596970a..19a080d 100644 --- a/test.js +++ b/test.js @@ -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;'; @@ -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, '""'); + 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 => {