Skip to content

Commit 9f9d002

Browse files
committed
feat: introduce a factory option in xception
1 parent b72cd3f commit 9f9d002

File tree

3 files changed

+88
-33
lines changed

3 files changed

+88
-33
lines changed

README.md

+38
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,44 @@ try {
182182

183183
---
184184

185+
### Method: xception
186+
187+
Convert an error to an Xception instance with metadata merged, preserving the original error message and stack.
188+
189+
**xception(exception: unknown, options?: Options): Xception**
190+
191+
| Parameter | Type | Description |
192+
| ------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
193+
| `exception` | unknown | a string, error, Xception, or object with a message property to be converted into an Xception instance |
194+
| `options.meta` | `Record<string, unknown>` | metadata to be embedded in the error |
195+
| `options.namespace` | `string` | an identifier of the component where the error occurred |
196+
| `options.tags` | `string[]` | tags for associating the error with specific contexts or categories |
197+
| `options.factory` | `(message: string, options: XceptionOptions) => Xception` | a custom factory function for creating `Xception` instances |
198+
199+
#### _/ Example /_
200+
201+
```ts
202+
import { xception } from 'xception';
203+
204+
try {
205+
throw new Error('original error message');
206+
} catch (e) {
207+
// convert to Xception with additional metadata
208+
const customError = xception(error, {
209+
meta: { key: 'value' },
210+
namespace: 'namespace',
211+
tags: ['critical'],
212+
factory: (message, options) => new CustomXception(message, options),
213+
});
214+
215+
throw customError;
216+
}
217+
```
218+
219+
This method allows for the transformation of existing errors into `Xception` instances, facilitating a unified approach to error handling and logging within applications.
220+
221+
---
222+
185223
## Know Issues & Limitations
186224

187225
- This package is designed to provide server-side debugging functionality only. It has not been tested on any web browsers.

source/xception.ts

+49-32
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,16 @@
1414
*/
1515

1616
import { isErrorLike } from '#isErrorLike';
17-
import { jsonify } from '#jsonify';
1817
import { Xception } from '#prototype';
1918
import { $meta, $namespace, $tags } from '#symbols';
2019

21-
import type { JsonObject } from 'type-fest';
22-
2320
import type { ErrorLike } from '#isErrorLike';
2421
import type { XceptionOptions } from '#prototype';
2522

23+
type Options = Omit<XceptionOptions, 'cause'> & {
24+
factory?: (message: string, options: XceptionOptions) => Xception;
25+
};
26+
2627
/**
2728
* convert an error to an Xception instance with metadata merged while preserving the original error message and stack
2829
* @param exception an exception to be converted
@@ -32,55 +33,71 @@ import type { XceptionOptions } from '#prototype';
3233
*/
3334
export default function xception(
3435
exception: unknown, // string, error, Xception, { message: string, ...}
35-
options?: Omit<XceptionOptions, 'cause'>,
36+
options?: Options,
3637
): Xception {
37-
const { namespace, meta = {}, tags = [] } = { ...options };
38+
// when options.factory is provided, it's used to create Xception instances; otherwise, the default constructor is used
39+
const factory =
40+
options?.factory ??
41+
((message: string, options?: XceptionOptions) =>
42+
new Xception(message, options));
3843

3944
if (isErrorLike(exception)) {
40-
const error = createXceptionFromError(exception);
41-
42-
// merge the namespace
43-
error[$namespace] = namespace ?? error[$namespace];
45+
// fetch defaults if provided in options, or use properties of the original exception
46+
const { namespace, meta, tags } = computeDefaults(exception, options);
4447

45-
// merge the meta data
46-
error[$meta] = { ...error[$meta], ...meta };
47-
48-
// merge the tags
49-
error[$tags] = [...new Set([...error[$tags], ...tags])];
48+
// create a new Xception with the original error's message and computed options
49+
const error = factory(exception.message, { namespace, meta, tags });
5050

5151
// replace the name and stack from the original error
5252
error.name = exception.name ?? error.name;
5353
error.stack = exception.stack ?? error.stack;
5454

5555
return error;
5656
} else if (typeof exception === 'string') {
57-
return new Xception(exception, options);
57+
// if exception is a string, we create a new Xception with the given message and options
58+
const { namespace, meta, tags } = { ...options };
59+
60+
return factory(exception, { namespace, meta, tags });
5861
}
5962

63+
// if exception is of unexpected type, throw a new Xception indicating the problem
6064
throw new Xception('unexpected exception type', {
6165
...options,
6266
cause: exception,
6367
});
6468
}
6569

6670
/**
67-
* convert an error-like object to an Xception instance
68-
* @param exception an exception to be converted
69-
* @returns the transformed error
71+
* compute defaults for namespace, meta, and tags by combining the ones present on the exception and the provided options
72+
* @param exception the original exception-like object
73+
* @param options additional options provided for generating the `Xception` instance
74+
* @returns an object containing computed namespace, meta, and tags to be used for the new `Xception`
7075
*/
71-
export function createXceptionFromError(exception: ErrorLike): Xception {
72-
if (exception instanceof Xception) {
73-
return exception;
74-
} else {
75-
const {
76-
name: _name,
77-
message: _message,
78-
stack: _stack,
79-
...meta
80-
} = jsonify(exception) as JsonObject;
76+
function computeDefaults(
77+
exception: ErrorLike,
78+
options?: Options,
79+
): {
80+
namespace?: string;
81+
meta: Record<string, unknown>;
82+
tags: string[];
83+
} {
84+
// if a namespace is provided in options, use it, otherwise use the original exception's namespace, if any
85+
const namespace =
86+
options?.namespace ?? (exception[$namespace] as string | undefined);
8187

82-
return new Xception(exception.message, {
83-
meta,
84-
});
85-
}
88+
// merge metadata from the original exception and options
89+
const meta = {
90+
...(exception[$meta] as Record<string, unknown>),
91+
...options?.meta,
92+
};
93+
94+
// concatenate and deduplicate tags from both the exception and the options
95+
const tags = [
96+
...new Set([
97+
...((exception[$tags] as string[] | undefined) ?? []),
98+
...(options?.tags ?? []),
99+
]),
100+
];
101+
102+
return { namespace, meta, tags };
86103
}

spec/xception.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515

1616
import { Xception } from '#prototype';
1717

18+
import { $meta } from '#symbols';
1819
import xception from '#xception';
19-
import { $cause, $meta } from '#symbols';
2020

2121
describe('fn:xceptionalize', () => {
2222
it('should wrap a string as an Xception', () => {

0 commit comments

Comments
 (0)