reslib 1.1.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -5
- package/build/auth/index.js +2 -2
- package/build/countries/index.js +2 -2
- package/build/currency/index.js +2 -2
- package/build/currency/session.js +2 -2
- package/build/exception/index.d.ts +1908 -0
- package/build/exception/index.js +5 -0
- package/build/i18n/index.d.ts +12 -4
- package/build/i18n/index.js +2 -2
- package/build/inputFormatter/index.js +2 -2
- package/build/logger/index.js +2 -2
- package/build/resources/ResourcePaginationHelper.js +1 -1
- package/build/resources/decorators/index.js +1 -1
- package/build/resources/fields/index.d.ts +5 -5
- package/build/resources/fields/index.js +1 -1
- package/build/resources/index.d.ts +1 -1
- package/build/resources/index.js +3 -3
- package/build/translations/index.d.ts +36 -4
- package/build/translations/index.js +2 -2
- package/build/translations/validator.en.d.ts +69 -4
- package/build/translations/validator.en.js +2 -2
- package/build/types/index.d.ts +21 -0
- package/build/utils/date/dateHelper.js +2 -2
- package/build/utils/date/index.js +2 -2
- package/build/utils/index.d.ts +2 -2
- package/build/utils/index.js +3 -3
- package/build/utils/interpolate.js +1 -1
- package/build/utils/isTime.js +1 -1
- package/build/utils/numbers.js +2 -2
- package/build/utils/object.js +1 -1
- package/build/validator/errors/index.d.ts +299 -0
- package/build/validator/errors/index.js +1 -0
- package/build/validator/index.d.ts +1 -0
- package/build/validator/index.js +3 -3
- package/build/validator/rules/array.js +2 -2
- package/build/validator/rules/boolean.js +2 -2
- package/build/validator/rules/date.js +2 -2
- package/build/validator/rules/default.js +2 -2
- package/build/validator/rules/enum.js +2 -2
- package/build/validator/rules/file.js +2 -2
- package/build/validator/rules/format.d.ts +13 -13
- package/build/validator/rules/format.js +3 -3
- package/build/validator/rules/index.js +3 -3
- package/build/validator/rules/multiRules.d.ts +10 -10
- package/build/validator/rules/multiRules.js +2 -2
- package/build/validator/rules/numeric.d.ts +8 -8
- package/build/validator/rules/numeric.js +2 -2
- package/build/validator/rules/object.js +2 -2
- package/build/validator/rules/string.d.ts +6 -6
- package/build/validator/rules/string.js +2 -2
- package/build/validator/rules/target.d.ts +8 -8
- package/build/validator/rules/target.js +2 -2
- package/build/validator/rules.types.d.ts +167 -0
- package/build/validator/rules.types.js +1 -0
- package/build/validator/types.d.ts +832 -1286
- package/build/validator/validator.d.ts +554 -867
- package/build/validator/validator.js +2 -2
- package/lib/cjs/exception.js +1 -0
- package/lib/esm/exception.mjs +2 -0
- package/package.json +6 -1
|
@@ -0,0 +1,1908 @@
|
|
|
1
|
+
import { ValidatorBulkError, ValidatorClassError, ValidatorError } from '../validator/errors';
|
|
2
|
+
/**
|
|
3
|
+
* Hook function type for intercepting exceptions (e.g., for logging).
|
|
4
|
+
*/
|
|
5
|
+
export type ExceptionHook = (exception: BaseException) => void;
|
|
6
|
+
/**
|
|
7
|
+
* Options for serializing the exception.
|
|
8
|
+
*/
|
|
9
|
+
export interface SerializationOptions {
|
|
10
|
+
/** Include the stack trace in the output. Default: false (unless not in production) */
|
|
11
|
+
stack?: boolean;
|
|
12
|
+
/** Include the cause chain in the output. Default: true */
|
|
13
|
+
cause?: boolean;
|
|
14
|
+
/** Max depth to serialize nested causes. Default: 10 */
|
|
15
|
+
maxCauseDepth?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Base application exception class.
|
|
19
|
+
* This class provides a standardized, serializable exception structure that can be
|
|
20
|
+
* safely returned from REST API endpoints, extended for domain-specific errors,
|
|
21
|
+
* and intercepted via hooks.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* // Basic usage
|
|
26
|
+
* throw new BaseException('User not found');
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // extending
|
|
31
|
+
* class PaymentException extends BaseException<{ transactionId: string }> {
|
|
32
|
+
* constructor(message: string, transactionId: string) {
|
|
33
|
+
* super(message, { details: { transactionId } });
|
|
34
|
+
* }
|
|
35
|
+
* }
|
|
36
|
+
*/
|
|
37
|
+
export declare class BaseException<TDetails = unknown, TCause = unknown> extends Error {
|
|
38
|
+
/**
|
|
39
|
+
* The constant name identifier for this exception class.
|
|
40
|
+
*/
|
|
41
|
+
static readonly NAME: "BaseException";
|
|
42
|
+
/**
|
|
43
|
+
* Unique marker property to identify BaseException instances (and subclasses)
|
|
44
|
+
* even after serialization/deserialization where instanceof check fails.
|
|
45
|
+
*/
|
|
46
|
+
readonly __isBaseException = true;
|
|
47
|
+
/**
|
|
48
|
+
* Global hooks that run when any BaseException (or subclass) is instantiated.
|
|
49
|
+
*/
|
|
50
|
+
private static hooks;
|
|
51
|
+
/**
|
|
52
|
+
* Application-specific error code (e.g., 'USER_NOT_FOUND').
|
|
53
|
+
*/
|
|
54
|
+
code?: string;
|
|
55
|
+
/**
|
|
56
|
+
* HTTP status code associated with this exception.
|
|
57
|
+
*/
|
|
58
|
+
statusCode?: number;
|
|
59
|
+
/**
|
|
60
|
+
* Additional structured details about the exception.
|
|
61
|
+
*/
|
|
62
|
+
details?: TDetails;
|
|
63
|
+
/**
|
|
64
|
+
* The underlying error that caused this exception.
|
|
65
|
+
*/
|
|
66
|
+
cause?: TCause;
|
|
67
|
+
/**
|
|
68
|
+
* Timestamp when the exception was created.
|
|
69
|
+
*/
|
|
70
|
+
readonly timestamp: Date;
|
|
71
|
+
readonly success: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* The validation error that caused this exception.
|
|
74
|
+
*/
|
|
75
|
+
validationError?: ValidatorError | ValidatorClassError | ValidatorBulkError;
|
|
76
|
+
/**
|
|
77
|
+
* Creates a new BaseException instance.
|
|
78
|
+
*
|
|
79
|
+
* @param message - Human-readable error message describing what went wrong
|
|
80
|
+
* @param options - Optional configuration object containing metadata like code, statusCode, details, cause, and timestamp
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* // Basic usage
|
|
85
|
+
* const error = new BaseException('User not found');
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```typescript
|
|
90
|
+
* // With error code and HTTP status
|
|
91
|
+
* const error = new BaseException('Invalid credentials', {
|
|
92
|
+
* code: 'AUTH_FAILED',
|
|
93
|
+
* statusCode: 401
|
|
94
|
+
* });
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```typescript
|
|
99
|
+
* // With typed details
|
|
100
|
+
* interface PaymentDetails {
|
|
101
|
+
* transactionId: string;
|
|
102
|
+
* amount: number;
|
|
103
|
+
* }
|
|
104
|
+
*
|
|
105
|
+
* const error = new BaseException<PaymentDetails>('Payment failed', {
|
|
106
|
+
* code: 'PAYMENT_ERROR',
|
|
107
|
+
* statusCode: 402,
|
|
108
|
+
* details: {
|
|
109
|
+
* transactionId: 'tx_12345',
|
|
110
|
+
* amount: 99.99
|
|
111
|
+
* }
|
|
112
|
+
* });
|
|
113
|
+
* ```
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```typescript
|
|
117
|
+
* // Wrapping another error as cause
|
|
118
|
+
* try {
|
|
119
|
+
* await database.query('SELECT * FROM users');
|
|
120
|
+
* } catch (dbError) {
|
|
121
|
+
* throw new BaseException('Database query failed', {
|
|
122
|
+
* code: 'DB_ERROR',
|
|
123
|
+
* cause: dbError // Original error preserved
|
|
124
|
+
* });
|
|
125
|
+
* }
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
constructor(message: string, options?: BaseExceptionOptions<TDetails, TCause>);
|
|
129
|
+
/**
|
|
130
|
+
* Converts the exception to a plain JSON-serializable object.
|
|
131
|
+
*
|
|
132
|
+
* This method creates a structured representation of the exception that can be safely
|
|
133
|
+
* sent over HTTP, stored in databases, or logged to external systems. The output includes
|
|
134
|
+
* all relevant exception metadata while avoiding circular references and non-serializable values.
|
|
135
|
+
*
|
|
136
|
+
* @param options - Optional configuration to control what gets included in the serialization
|
|
137
|
+
* @param options.stack - Whether to include the stack trace. Defaults to true in non-production, false in production
|
|
138
|
+
* @param options.cause - Whether to include the error cause chain. Defaults to true
|
|
139
|
+
* @param options.maxCauseDepth - Maximum depth for serializing nested causes. Defaults to 10
|
|
140
|
+
* @returns A plain object suitable for JSON.stringify() or API responses
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```typescript
|
|
144
|
+
* // Basic serialization
|
|
145
|
+
* const error = new BaseException('User not found', {
|
|
146
|
+
* code: 'USER_NOT_FOUND',
|
|
147
|
+
* statusCode: 404
|
|
148
|
+
* });
|
|
149
|
+
*
|
|
150
|
+
* console.log(error.toJSON());
|
|
151
|
+
* // {
|
|
152
|
+
* // __isBaseException: true,
|
|
153
|
+
* // __baseExceptionName: 'BaseException',
|
|
154
|
+
* // name: 'BaseException',
|
|
155
|
+
* // message: 'User not found',
|
|
156
|
+
* // code: 'USER_NOT_FOUND',
|
|
157
|
+
* // statusCode: 404,
|
|
158
|
+
* // timestamp: '2024-01-15T10:30:00.000Z',
|
|
159
|
+
* // success: false
|
|
160
|
+
* // }
|
|
161
|
+
* ```
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* ```typescript
|
|
165
|
+
* // Serialize with stack trace for debugging
|
|
166
|
+
* const error = new BaseException('Database connection failed');
|
|
167
|
+
* const serialized = error.toJSON({ stack: true });
|
|
168
|
+
*
|
|
169
|
+
* console.log(serialized.stack); // Full stack trace included
|
|
170
|
+
* ```
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```typescript
|
|
174
|
+
* // API response usage
|
|
175
|
+
* app.use((err, req, res, next) => {
|
|
176
|
+
* if (BaseException.is(err)) {
|
|
177
|
+
* res.status(err.statusCode || 500).json(err.toJSON({
|
|
178
|
+
* stack: process.env.NODE_ENV !== 'production',
|
|
179
|
+
* cause: true
|
|
180
|
+
* }));
|
|
181
|
+
* }
|
|
182
|
+
* });
|
|
183
|
+
* ```
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```typescript
|
|
187
|
+
* // Serializing error chains
|
|
188
|
+
* const dbError = new Error('Connection timeout');
|
|
189
|
+
* const appError = new BaseException('Failed to fetch user', {
|
|
190
|
+
* cause: dbError
|
|
191
|
+
* });
|
|
192
|
+
*
|
|
193
|
+
* const json = appError.toJSON({ cause: true });
|
|
194
|
+
* console.log(json.cause);
|
|
195
|
+
* // {
|
|
196
|
+
* // name: 'Error',
|
|
197
|
+
* // message: 'Connection timeout'
|
|
198
|
+
* // }
|
|
199
|
+
* ```
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* ```typescript
|
|
203
|
+
* // With custom details
|
|
204
|
+
* interface PaymentDetails {
|
|
205
|
+
* transactionId: string;
|
|
206
|
+
* amount: number;
|
|
207
|
+
* }
|
|
208
|
+
*
|
|
209
|
+
* const error = new BaseException<PaymentDetails>('Payment failed', {
|
|
210
|
+
* code: 'PAYMENT_FAILED',
|
|
211
|
+
* details: {
|
|
212
|
+
* transactionId: 'tx_12345',
|
|
213
|
+
* amount: 99.99
|
|
214
|
+
* }
|
|
215
|
+
* });
|
|
216
|
+
*
|
|
217
|
+
* console.log(error.toJSON().details);
|
|
218
|
+
* // { transactionId: 'tx_12345', amount: 99.99 }
|
|
219
|
+
* ```
|
|
220
|
+
*
|
|
221
|
+
* @remarks
|
|
222
|
+
* - Always includes `__isBaseException: true` marker for runtime type checking
|
|
223
|
+
* - Timestamps are converted to ISO 8601 strings
|
|
224
|
+
* - Circular references in causes are prevented via maxCauseDepth
|
|
225
|
+
* - Stack traces are excluded by default in production for security
|
|
226
|
+
* - Compatible with JSON.stringify() and structured clone algorithms
|
|
227
|
+
*/
|
|
228
|
+
toJSON(options?: SerializationOptions): Record<string, unknown>;
|
|
229
|
+
/**
|
|
230
|
+
* Helper to Recursively serialize causes.
|
|
231
|
+
*/
|
|
232
|
+
private serializeCause;
|
|
233
|
+
/**
|
|
234
|
+
* Creates a human-readable string representation of the exception.
|
|
235
|
+
*
|
|
236
|
+
* Returns a formatted string in the format: `Name [CODE]: Message` or `Name: Message` if no code is set.
|
|
237
|
+
* This is automatically called when the exception is converted to a string (e.g., when logged).
|
|
238
|
+
*
|
|
239
|
+
* @returns Formatted string representation
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* ```typescript
|
|
243
|
+
* // Without code
|
|
244
|
+
* const error = new BaseException('User not found');
|
|
245
|
+
* console.log(error.toString());
|
|
246
|
+
* // "BaseException: User not found"
|
|
247
|
+
* ```
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* ```typescript
|
|
251
|
+
* // With error code
|
|
252
|
+
* const error = new BaseException('Invalid credentials', {
|
|
253
|
+
* code: 'AUTH_FAILED'
|
|
254
|
+
* });
|
|
255
|
+
* console.log(error.toString());
|
|
256
|
+
* // "BaseException [AUTH_FAILED]: Invalid credentials"
|
|
257
|
+
* ```
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* ```typescript
|
|
261
|
+
* // String coercion
|
|
262
|
+
* const error = new BaseException('Payment failed', { code: 'PAYMENT_ERROR' });
|
|
263
|
+
* console.log('Error: ' + error);
|
|
264
|
+
* // "Error: BaseException [PAYMENT_ERROR]: Payment failed"
|
|
265
|
+
* ```
|
|
266
|
+
*
|
|
267
|
+
* @example
|
|
268
|
+
* ```typescript
|
|
269
|
+
* // In subclasses
|
|
270
|
+
* class ApiException extends BaseException {
|
|
271
|
+
* // toString automatically uses the subclass name
|
|
272
|
+
* }
|
|
273
|
+
*
|
|
274
|
+
* const error = new ApiException('Request failed', { code: 'API_ERROR' });
|
|
275
|
+
* console.log(error.toString());
|
|
276
|
+
* // "ApiException [API_ERROR]: Request failed"
|
|
277
|
+
* ```
|
|
278
|
+
*/
|
|
279
|
+
toString(): string;
|
|
280
|
+
/**
|
|
281
|
+
* Custom instanceof check. When consumers import `BaseException` from built packages or
|
|
282
|
+
* across module boundaries, class identity can differ. Using Symbol.hasInstance
|
|
283
|
+
* allows `instanceof BaseException` to succeed if the object has the required BaseException API
|
|
284
|
+
* shape (duck typing). This preserves `instanceof` checks externally while
|
|
285
|
+
* keeping the current exported API intact.
|
|
286
|
+
*/
|
|
287
|
+
/**
|
|
288
|
+
* Type guard to check if a value is a BaseException instance or has BaseException structure.
|
|
289
|
+
*
|
|
290
|
+
* This method performs both runtime `instanceof` checks and duck-typing validation.
|
|
291
|
+
* It works correctly even with:
|
|
292
|
+
* - Serialized/deserialized exceptions from JSON
|
|
293
|
+
* - Exceptions from different module boundaries
|
|
294
|
+
* - Objects that match the BaseException shape
|
|
295
|
+
*
|
|
296
|
+
* @template TException - The specific exception type to check for (defaults to BaseException)
|
|
297
|
+
* @param value - The value to check
|
|
298
|
+
* @returns True if the value is a BaseException or matches its structure
|
|
299
|
+
*
|
|
300
|
+
* @example
|
|
301
|
+
* ```typescript
|
|
302
|
+
* // Check exception instances
|
|
303
|
+
* const error = new BaseException('Error');
|
|
304
|
+
* if (BaseException.is(error)) {
|
|
305
|
+
* console.log(error.code); // TypeScript knows it's a BaseException
|
|
306
|
+
* }
|
|
307
|
+
* ```
|
|
308
|
+
*
|
|
309
|
+
* @example
|
|
310
|
+
* ```typescript
|
|
311
|
+
* // Works with serialized exceptions
|
|
312
|
+
* const error = new BaseException('Test', { code: 'TEST' });
|
|
313
|
+
* const json = JSON.stringify(error);
|
|
314
|
+
* const parsed = JSON.parse(json);
|
|
315
|
+
*
|
|
316
|
+
* BaseException.is(parsed); // true! (duck-typing)
|
|
317
|
+
* ```
|
|
318
|
+
*
|
|
319
|
+
* @example
|
|
320
|
+
* ```typescript
|
|
321
|
+
* // Type narrowing in error handlers
|
|
322
|
+
* function handleError(error: unknown) {
|
|
323
|
+
* if (BaseException.is(error)) {
|
|
324
|
+
* // TypeScript narrows type to BaseException
|
|
325
|
+
* console.log('Code:', error.code);
|
|
326
|
+
* console.log('Status:', error.statusCode);
|
|
327
|
+
* console.log('Details:', error.details);
|
|
328
|
+
* } else {
|
|
329
|
+
* console.log('Unknown error:', error);
|
|
330
|
+
* }
|
|
331
|
+
* }
|
|
332
|
+
* ```
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* ```typescript
|
|
336
|
+
* // Filter exceptions from array
|
|
337
|
+
* const errors: unknown[] = [
|
|
338
|
+
* new BaseException('Error 1'),
|
|
339
|
+
* new Error('Error 2'),
|
|
340
|
+
* 'string error',
|
|
341
|
+
* { message: 'plain object' }
|
|
342
|
+
* ];
|
|
343
|
+
*
|
|
344
|
+
* const baseExceptions = errors.filter(BaseException.is);
|
|
345
|
+
* // baseExceptions is typed as BaseException[]
|
|
346
|
+
* ```
|
|
347
|
+
*
|
|
348
|
+
* @example
|
|
349
|
+
* ```typescript
|
|
350
|
+
* // Check for specific subclass
|
|
351
|
+
* class ApiException extends BaseException {}
|
|
352
|
+
*
|
|
353
|
+
* const error: unknown = new ApiException('API failed');
|
|
354
|
+
* if (BaseException.is<ApiException>(error)) {
|
|
355
|
+
* // error is narrowed to ApiException
|
|
356
|
+
* }
|
|
357
|
+
* ```
|
|
358
|
+
*
|
|
359
|
+
* @remarks
|
|
360
|
+
* The duck-typing check verifies:
|
|
361
|
+
* - `__isBaseException === true`
|
|
362
|
+
* - `message` is a string
|
|
363
|
+
* - `success === false`
|
|
364
|
+
* - `__baseExceptionName === 'BaseException'`
|
|
365
|
+
*
|
|
366
|
+
* This ensures compatibility across module boundaries and serialization.
|
|
367
|
+
*/
|
|
368
|
+
static is<TException extends BaseException>(value: unknown): value is TException;
|
|
369
|
+
/**
|
|
370
|
+
* Protected factory method to create a new exception instance.
|
|
371
|
+
*
|
|
372
|
+
* **Purpose**: Low-level factory that creates a NEW exception instance from a message and options.
|
|
373
|
+
* This is the final step in the exception creation chain used by `createFromError()`.
|
|
374
|
+
*
|
|
375
|
+
* **Key Differences from Related Methods**:
|
|
376
|
+
* - **`create(message, options)`** ← YOU ARE HERE
|
|
377
|
+
* - Creates NEW instance from known message + options
|
|
378
|
+
* - Simple: `new this(message, options)`
|
|
379
|
+
* - Override for: post-construction logic (DI, logging, tracking)
|
|
380
|
+
*
|
|
381
|
+
* - **`createFromError(error, options)`** ← Higher-level
|
|
382
|
+
* - Converts UNKNOWN error → extracts data → calls `create()`
|
|
383
|
+
* - Complex: error detection, parsing, then calls create()
|
|
384
|
+
* - Override for: domain-specific error detection (DB codes, HTTP status)
|
|
385
|
+
*
|
|
386
|
+
* - **`withOptions(exception, options)`** ← Different purpose
|
|
387
|
+
* - Mutates EXISTING instance (no new instance created)
|
|
388
|
+
* - Updates properties on already-created exception
|
|
389
|
+
* - Use for: adding metadata to existing exceptions
|
|
390
|
+
*
|
|
391
|
+
* **Flow**: `from(error)` → `createFromError(error)` → `parseErrorMessage()` + `parseErrorDetails()` → **`create(message, options)`** → `new this()`
|
|
392
|
+
*
|
|
393
|
+
* @template TDetails - Type of the details object
|
|
394
|
+
* @template TException - The exception type being created
|
|
395
|
+
* @param this - The exception constructor (auto-bound)
|
|
396
|
+
* @param message - The error message (already extracted/formatted)
|
|
397
|
+
* @param options - Optional exception metadata (code, statusCode, details, etc.)
|
|
398
|
+
* @returns A new exception instance of the calling class type
|
|
399
|
+
*
|
|
400
|
+
* @example
|
|
401
|
+
* ```typescript
|
|
402
|
+
* // Internal usage (this is what from() does internally)
|
|
403
|
+
* class ApiException extends BaseException {
|
|
404
|
+
* // The create method is inherited and works polymorphically
|
|
405
|
+
* }
|
|
406
|
+
*
|
|
407
|
+
* // When ApiException.from() calls create internally:
|
|
408
|
+
* const instance = ApiException.create('Error', { code: 'API_ERROR' });
|
|
409
|
+
* // instance is typed as ApiException, not BaseException
|
|
410
|
+
* ```
|
|
411
|
+
*
|
|
412
|
+
* @example
|
|
413
|
+
* ```typescript
|
|
414
|
+
* // Override for custom initialization
|
|
415
|
+
* class TrackedEvent {
|
|
416
|
+
* constructor(public eventId: string) {}
|
|
417
|
+
* }
|
|
418
|
+
*
|
|
419
|
+
* class TrackedExceptio extends BaseException {
|
|
420
|
+
* public tracker?: TrackedEvent;
|
|
421
|
+
*
|
|
422
|
+
* protected static override create<
|
|
423
|
+
* TDetails = unknown,
|
|
424
|
+
* T extends BaseException<TDetails> = BaseException<TDetails>
|
|
425
|
+
* >(
|
|
426
|
+
* this: BaseExceptionConstructor<TDetails, T>,
|
|
427
|
+
* message: string,
|
|
428
|
+
* options?: BaseExceptionOptions<TDetails>
|
|
429
|
+
* ): T {
|
|
430
|
+
* const instance = new this(message, options);
|
|
431
|
+
*
|
|
432
|
+
* // Custom post-creation logic
|
|
433
|
+
* if (instance instanceof TrackedExceptio) {
|
|
434
|
+
* instance.tracker = new TrackedEvent(generateId());
|
|
435
|
+
* }
|
|
436
|
+
*
|
|
437
|
+
* return instance;
|
|
438
|
+
* }
|
|
439
|
+
* }
|
|
440
|
+
* ```
|
|
441
|
+
*
|
|
442
|
+
* @example
|
|
443
|
+
* ```typescript
|
|
444
|
+
* // Dependency injection pattern
|
|
445
|
+
* class ServiceException extends BaseException {
|
|
446
|
+
* private static logger?: Logger;
|
|
447
|
+
*
|
|
448
|
+
* static setLogger(logger: Logger) {
|
|
449
|
+
* this.logger = logger;
|
|
450
|
+
* }
|
|
451
|
+
*
|
|
452
|
+
* protected static override create<
|
|
453
|
+
* TDetails = unknown,
|
|
454
|
+
* T extends BaseException<TDetails> = BaseException<TDetails>
|
|
455
|
+
* >(
|
|
456
|
+
* this: BaseExceptionConstructor<TDetails, T>,
|
|
457
|
+
* message: string,
|
|
458
|
+
* options?: BaseExceptionOptions<TDetails>
|
|
459
|
+
* ): T {
|
|
460
|
+
* const instance = new this(message, options);
|
|
461
|
+
*
|
|
462
|
+
* // Auto-log on creation
|
|
463
|
+
* if (this.logger) {
|
|
464
|
+
* this.logger.error(`[${instance.code}] ${message}`);
|
|
465
|
+
* }
|
|
466
|
+
*
|
|
467
|
+
* return instance;
|
|
468
|
+
* }
|
|
469
|
+
* }
|
|
470
|
+
* ```
|
|
471
|
+
*
|
|
472
|
+
* @remarks
|
|
473
|
+
* **When to override `create()`**:
|
|
474
|
+
* - Post-construction initialization (add tracking IDs, timestamps)
|
|
475
|
+
* - Dependency injection (inject logger, metrics service)
|
|
476
|
+
* - Automatic logging/monitoring on every exception creation
|
|
477
|
+
* - Set default properties based on application context
|
|
478
|
+
*
|
|
479
|
+
* **When NOT to override `create()` (use other methods)**:
|
|
480
|
+
* - Error detection/parsing → use `createFromError()` instead
|
|
481
|
+
* - Message extraction → use `parseErrorMessage()` instead
|
|
482
|
+
* - Detail extraction → use `parseErrorDetails()` instead
|
|
483
|
+
* - Updating existing exceptions → use `withOptions()` instead
|
|
484
|
+
*
|
|
485
|
+
* **Default implementation**: `new this(message, options)` - Creates instance with polymorphic type.
|
|
486
|
+
*
|
|
487
|
+
* **Method Comparison**:
|
|
488
|
+
* ```typescript
|
|
489
|
+
* // create: NEW instance from message
|
|
490
|
+
* const ex1 = create('Error', { code: 'ERR' }); // NEW BaseException
|
|
491
|
+
*
|
|
492
|
+
* // createFromError: EXTRACT data from error, then create
|
|
493
|
+
* const ex2 = createFromError(dbError, {}); // Extracts → create()
|
|
494
|
+
*
|
|
495
|
+
* // withOptions: MUTATE existing instance
|
|
496
|
+
* const ex3 = withOptions(ex1, { code: 'NEW' }); // Same instance, code changed
|
|
497
|
+
* console.log(ex3 === ex1); // true (mutated, not new)
|
|
498
|
+
* ```
|
|
499
|
+
*
|
|
500
|
+
* @see {@link createFromError} - Use this for error conversion logic
|
|
501
|
+
* @see {@link withOptions} - Use this to update existing exceptions
|
|
502
|
+
* @see {@link from} - Public method that orchestrates the creation flow
|
|
503
|
+
*/
|
|
504
|
+
protected static create<TDetails = unknown, TException extends BaseException<TDetails> = BaseException<TDetails>, TCause = unknown>(this: BaseExceptionConstructor<TDetails, TException>, message: string, options?: BaseExceptionOptions<TDetails, TCause>): TException;
|
|
505
|
+
/**
|
|
506
|
+
* Creates a BaseException instance from any unknown error value.
|
|
507
|
+
*
|
|
508
|
+
* This is the primary factory method for creating exceptions from various error sources.
|
|
509
|
+
* It intelligently handles different input types:
|
|
510
|
+
* - Error objects (preserves message, stack, cause)
|
|
511
|
+
* - JSON strings (automatically parses)
|
|
512
|
+
* - Plain objects (extracts message and details)
|
|
513
|
+
* - Strings (uses as message)
|
|
514
|
+
* - Existing BaseException instances (merges options)
|
|
515
|
+
*
|
|
516
|
+
* **Extensibility**: Subclasses can override `parseErrorMessage`, `parseErrorDetails`, or
|
|
517
|
+
* `createFromError` to customize how errors are converted.
|
|
518
|
+
*
|
|
519
|
+
* @template TDetails - Type of the details object
|
|
520
|
+
* @template TException - The exception type being created (automatically inferred from the class)
|
|
521
|
+
* @param this - The exception constructor (automatically bound when called as `ClassName.from()`)
|
|
522
|
+
* @param error - The error value to convert (any type)
|
|
523
|
+
* @param options - Optional metadata to merge/override
|
|
524
|
+
* @returns A new exception instance of the calling class type
|
|
525
|
+
*
|
|
526
|
+
* @example
|
|
527
|
+
* ```typescript
|
|
528
|
+
* // Convert standard Error
|
|
529
|
+
* try {
|
|
530
|
+
* JSON.parse('invalid');
|
|
531
|
+
* } catch (err) {
|
|
532
|
+
* throw BaseException.from(err);
|
|
533
|
+
* // BaseException with err.message, err.stack preserved
|
|
534
|
+
* }
|
|
535
|
+
* ```
|
|
536
|
+
*
|
|
537
|
+
* @example
|
|
538
|
+
* ```typescript
|
|
539
|
+
* // From string
|
|
540
|
+
* const error = BaseException.from('Something went wrong');
|
|
541
|
+
* console.log(error.message); // \"Something went wrong\"
|
|
542
|
+
* ```
|
|
543
|
+
*
|
|
544
|
+
* @example
|
|
545
|
+
* ```typescript
|
|
546
|
+
* // From plain object (API error responses)\n * const apiError = {\n * message: 'Validation failed',\n * code: 'VALIDATION_ERROR',\n * statusCode: 400,\n * errors: [{ field: 'email', message: 'Invalid format' }]\n * };\n *\n * const exception = BaseException.from(apiError);\n * console.log(exception.code); // 'VALIDATION_ERROR'\n * console.log(exception.statusCode); // 400\n * console.log(exception.details); // Contains the API error details\n * ```\n *\n * @example\n * ```typescript\n * // From JSON string\n * const jsonError = '{\"message\":\"Error\",\"code\":\"ERR_001\"}';\n * const error = BaseException.from(jsonError);\n * // Automatically parsed and converted\n * ```\n *\n * @example\n * ```typescript\n * // With additional options\n * const dbError = new Error('Connection timeout');\n * const appError = BaseException.from(dbError, {\n * code: 'DB_CONNECTION_ERROR',\n * statusCode: 503,\n * details: { database: 'users_db', timeout: 5000 }\n * });\n * ```\n *\n * @example\n * ```typescript\n * // Subclass usage - maintains type\n * class ApiException extends BaseException<{ endpoint: string }> {\n * protected static override parseErrorDetails(error: unknown) {\n * const details = super.parseErrorDetails(error);\n * return {\n * ...details,\n * endpoint: typeof error === 'object' && error !== null \n * ? (error as any).url || (error as any).endpoint\n * : undefined\n * };\n * }\n * }\n *\n * const apiError = { message: 'Failed', url: '/api/users' };\n * const exception = ApiException.from(apiError);\n * // exception is typed as ApiException\n * console.log(exception.details?.endpoint); // '/api/users'\n * ```\n *\n * @example\n * ```typescript\n * // Error chain preservation\n * const rootCause = new Error('Network timeout');\n * const wrapped = BaseException.from(rootCause, {\n * code: 'NETWORK_ERROR'\n * });\n *\n * console.log(wrapped.cause === rootCause); // true\n * console.log(wrapped.toJSON().cause); // Serialized root cause\n * ```\n *\n * @example\n * ```typescript\n * // Re-using existing exception with new options\n * const original = new BaseException('Error');\n * const updated = BaseException.from(original, {\n * code: 'UPDATED_CODE'\n * });\n *\n * console.log(updated === original); // true (same instance)\n * console.log(updated.code); // 'UPDATED_CODE' (merged)\n * ```\n *\n * @remarks\n * **Behavior**:\n * - If `error` is already an instance of the calling class, returns it with options merged\n * - Automatically detects and parses JSON strings\n * - Extracts `code` and `statusCode` from error objects when available\n * - Preserves error chains via the `cause` property\n * - Sets `statusCode` to 500 by default for unknown errors\n * - Override `parseErrorMessage()` to customize message extraction\n * - Override `parseErrorDetails()` to extract custom fields\n * - Override `createFromError()` for complete control over error conversion\n * - Override `from()` itself for pre/post-processing logic\n *\n * @see {@link parseErrorMessage} - Customize message extraction\n * @see {@link parseErrorDetails} - Customize details extraction\n * @see {@link createFromError} - Full control over error conversion\n * @see {@link withOptions} - Merge options into existing exceptions\n */
|
|
547
|
+
static from<TDetails = unknown, TException extends BaseException<TDetails> = BaseException<TDetails>, TCause = unknown>(this: BaseExceptionConstructor<TDetails, TException>, error: unknown, options?: BaseExceptionOptions<TDetails, TCause>): TException;
|
|
548
|
+
/**
|
|
549
|
+
* Protected method to create an exception from an error source.
|
|
550
|
+
*
|
|
551
|
+
* This is the **core conversion method** called by `from()` after it has handled
|
|
552
|
+
* JSON parsing and existing instance checks. Override this method in subclasses
|
|
553
|
+
* to implement custom error handling logic, such as detecting specific error types
|
|
554
|
+
* and setting appropriate codes, status codes, and details.
|
|
555
|
+
*
|
|
556
|
+
* The default implementation:
|
|
557
|
+
* 1. Calls `parseErrorMessage(error, options)` to extract the message
|
|
558
|
+
* 2. Calls `parseErrorDetails(error, options)` to extract structured details
|
|
559
|
+
* 3. Determines the final `code` from options, details.code, or details.errorCode
|
|
560
|
+
* 4. Determines the final `statusCode` from options, details.statusCode, or defaults to 500
|
|
561
|
+
* 5. Creates a new exception instance via `create()` with all extracted data
|
|
562
|
+
* 6. Sets the original error as the `cause`
|
|
563
|
+
*
|
|
564
|
+
* **Protected**: This is an extensibility hook. Override this when you need complete
|
|
565
|
+
* control over how errors are converted to exceptions for domain-specific logic.
|
|
566
|
+
*
|
|
567
|
+
* @template TDetails - Type of the details object
|
|
568
|
+
* @template TException - The exception type being created
|
|
569
|
+
* @param this - The exception constructor (auto-bound)
|
|
570
|
+
* @param error - The error value to convert (any type)
|
|
571
|
+
* @param options - Optional metadata to merge/override
|
|
572
|
+
* @returns A new exception instance with extracted data
|
|
573
|
+
*
|
|
574
|
+
* @example
|
|
575
|
+
* ```typescript
|
|
576
|
+
* // Database exception with error code detection
|
|
577
|
+
* interface DbErrorDetails {
|
|
578
|
+
* query?: string;
|
|
579
|
+
* table?: string;
|
|
580
|
+
* constraint?: string;
|
|
581
|
+
* }
|
|
582
|
+
*
|
|
583
|
+
* class DatabaseException extends BaseException<DbErrorDetails> {
|
|
584
|
+
* protected static override createFromError<
|
|
585
|
+
* TDetails = DbErrorDetails,
|
|
586
|
+
* T extends BaseException<TDetails> = BaseException<TDetails>
|
|
587
|
+
* >(
|
|
588
|
+
* this: BaseExceptionConstructor<TDetails, T>,
|
|
589
|
+
* error: unknown,
|
|
590
|
+
* options?: BaseExceptionOptions<TDetails>
|
|
591
|
+
* ): T {
|
|
592
|
+
* if (typeof error === 'object' && error !== null) {
|
|
593
|
+
* const err = error as Record<string, unknown>;
|
|
594
|
+
*
|
|
595
|
+
* // PostgreSQL duplicate key error
|
|
596
|
+
* if (err.code === '23505') {
|
|
597
|
+
* return new this('Duplicate entry detected', {
|
|
598
|
+
* ...options,
|
|
599
|
+
* code: 'DB_DUPLICATE_ENTRY',
|
|
600
|
+
* statusCode: 409,
|
|
601
|
+
* details: {
|
|
602
|
+
* ...options?.details,
|
|
603
|
+
* table: err.table as string,
|
|
604
|
+
* constraint: err.constraint as string,
|
|
605
|
+
* query: err.query as string
|
|
606
|
+
* } as TDetails,
|
|
607
|
+
* cause: error
|
|
608
|
+
* });
|
|
609
|
+
* }
|
|
610
|
+
*
|
|
611
|
+
* // Foreign key constraint error
|
|
612
|
+
* if (err.code === '23503') {
|
|
613
|
+
* return new this('Foreign key violation', {
|
|
614
|
+
* ...options,
|
|
615
|
+
* code: 'DB_FK_VIOLATION',
|
|
616
|
+
* statusCode: 400,
|
|
617
|
+
* details: {
|
|
618
|
+
* ...options?.details,
|
|
619
|
+
* table: err.table as string,
|
|
620
|
+
* constraint: err.constraint as string
|
|
621
|
+
* } as TDetails,
|
|
622
|
+
* cause: error
|
|
623
|
+
* });
|
|
624
|
+
* }
|
|
625
|
+
* }
|
|
626
|
+
*
|
|
627
|
+
* // Fall back to default behavior for unknown errors
|
|
628
|
+
* return super.createFromError(error, options) as T;
|
|
629
|
+
* }
|
|
630
|
+
* }
|
|
631
|
+
* ```
|
|
632
|
+
*
|
|
633
|
+
* @example
|
|
634
|
+
* ```typescript
|
|
635
|
+
* // HTTP exception with status code mapping
|
|
636
|
+
* interface HttpErrorDetails {
|
|
637
|
+
* statusCode: number;
|
|
638
|
+
* path?: string;
|
|
639
|
+
* method?: string;
|
|
640
|
+
* headers?: Record<string, string>;
|
|
641
|
+
* }
|
|
642
|
+
*
|
|
643
|
+
* class HttpException extends BaseException<HttpErrorDetails> {
|
|
644
|
+
* protected static override createFromError<
|
|
645
|
+
* TDetails = HttpErrorDetails,
|
|
646
|
+
* T extends BaseException<TDetails> = BaseException<TDetails>
|
|
647
|
+
* >(
|
|
648
|
+
* this: BaseExceptionConstructor<TDetails, T>,
|
|
649
|
+
* error: unknown,
|
|
650
|
+
* options?: BaseExceptionOptions<TDetails>
|
|
651
|
+
* ): T {
|
|
652
|
+
* if (typeof error === 'object' && error !== null) {
|
|
653
|
+
* const err = error as Record<string, unknown>;
|
|
654
|
+
* const status = typeof err.statusCode === 'number' ? err.statusCode : 500;
|
|
655
|
+
*
|
|
656
|
+
* // Map status codes to standard messages
|
|
657
|
+
* const statusMessages: Record<number, string> = {
|
|
658
|
+
* 400: 'Bad Request',
|
|
659
|
+
* 401: 'Unauthorized',
|
|
660
|
+
* 403: 'Forbidden',
|
|
661
|
+
* 404: 'Not Found',
|
|
662
|
+
* 500: 'Internal Server Error',
|
|
663
|
+
* 502: 'Bad Gateway',
|
|
664
|
+
* 503: 'Service Unavailable'
|
|
665
|
+
* };
|
|
666
|
+
*
|
|
667
|
+
* const message = typeof err.message === 'string'
|
|
668
|
+
* ? err.message
|
|
669
|
+
* : statusMessages[status] || 'HTTP Error';
|
|
670
|
+
*
|
|
671
|
+
* return new this(message, {
|
|
672
|
+
* ...options,
|
|
673
|
+
* code: `HTTP_${status}`,
|
|
674
|
+
* statusCode: status,
|
|
675
|
+
* details: {
|
|
676
|
+
* ...options?.details,
|
|
677
|
+
* statusCode: status,
|
|
678
|
+
* path: typeof err.path === 'string' ? err.path : undefined,
|
|
679
|
+
* method: typeof err.method === 'string' ? err.method : undefined,
|
|
680
|
+
* headers: err.headers as Record<string, string>
|
|
681
|
+
* } as TDetails,
|
|
682
|
+
* cause: error
|
|
683
|
+
* });
|
|
684
|
+
* }
|
|
685
|
+
*
|
|
686
|
+
* return super.createFromError(error, options) as T;
|
|
687
|
+
* }
|
|
688
|
+
* }
|
|
689
|
+
* ```
|
|
690
|
+
*
|
|
691
|
+
* @example
|
|
692
|
+
* ```typescript
|
|
693
|
+
* // Validation exception with error aggregation
|
|
694
|
+
* interface ValidationErrorDetails {
|
|
695
|
+
* errors: Array<{ field: string; message: string; rule: string }>;
|
|
696
|
+
* }
|
|
697
|
+
*
|
|
698
|
+
* class ValidationException extends BaseException<ValidationErrorDetails> {
|
|
699
|
+
* protected static override createFromError<
|
|
700
|
+
* TDetails = ValidationErrorDetails,
|
|
701
|
+
* T extends BaseException<TDetails> = BaseException<TDetails>
|
|
702
|
+
* >(
|
|
703
|
+
* this: BaseExceptionConstructor<TDetails, T>,
|
|
704
|
+
* error: unknown,
|
|
705
|
+
* options?: BaseExceptionOptions<TDetails>
|
|
706
|
+
* ): T {
|
|
707
|
+
* if (typeof error === 'object' && error !== null) {
|
|
708
|
+
* const err = error as Record<string, unknown>;
|
|
709
|
+
*
|
|
710
|
+
* // Check for validation error array (Joi, Yup, etc.)
|
|
711
|
+
* if (Array.isArray(err.details) && err.details.length > 0) {
|
|
712
|
+
* const validationErrors = err.details.map((detail: any) => ({
|
|
713
|
+
* field: detail.path?.join('.') || 'unknown',
|
|
714
|
+
* message: detail.message || 'Validation failed',
|
|
715
|
+
* rule: detail.type || 'unknown'
|
|
716
|
+
* }));
|
|
717
|
+
*
|
|
718
|
+
* const firstError = validationErrors[0];
|
|
719
|
+
* const message = validationErrors.length === 1
|
|
720
|
+
* ? `Validation failed for ${firstError.field}: ${firstError.message}`
|
|
721
|
+
* : `Validation failed for ${validationErrors.length} fields`;
|
|
722
|
+
*
|
|
723
|
+
* return new this(message, {
|
|
724
|
+
* ...options,
|
|
725
|
+
* code: 'VALIDATION_ERROR',
|
|
726
|
+
* statusCode: 400,
|
|
727
|
+
* details: {
|
|
728
|
+
* ...options?.details,
|
|
729
|
+
* errors: validationErrors
|
|
730
|
+
* } as TDetails,
|
|
731
|
+
* cause: error
|
|
732
|
+
* });
|
|
733
|
+
* }
|
|
734
|
+
* }
|
|
735
|
+
*
|
|
736
|
+
* return super.createFromError(error, options) as T;
|
|
737
|
+
* }
|
|
738
|
+
* }
|
|
739
|
+
* ```
|
|
740
|
+
*
|
|
741
|
+
* @remarks
|
|
742
|
+
* **When to override `createFromError()`**:
|
|
743
|
+
* - Domain-specific error detection (e.g., database error codes, HTTP status)
|
|
744
|
+
* - Map error types to standardized codes/messages
|
|
745
|
+
* - Aggregate or transform complex error structures
|
|
746
|
+
* - Complete control over error-to-exception conversion
|
|
747
|
+
*
|
|
748
|
+
* **When NOT to override `createFromError()` (use other methods)**:
|
|
749
|
+
* - Only custom message formatting → use `parseErrorMessage()` instead
|
|
750
|
+
* - Only custom detail extraction → use `parseErrorDetails()` instead
|
|
751
|
+
* - Simple pre/post processing → override `from()` itself
|
|
752
|
+
* - Post-construction logic → use `create()` instead
|
|
753
|
+
*
|
|
754
|
+
* **Key Differences from Related Methods**:
|
|
755
|
+
* - **`createFromError(error, options)`** ← YOU ARE HERE
|
|
756
|
+
* - Converts UNKNOWN error → extracts data → creates NEW instance
|
|
757
|
+
* - Complex: detects error type, parses fields, calls `create()`
|
|
758
|
+
* - Override for: error detection (DB codes, HTTP status, validation)
|
|
759
|
+
*
|
|
760
|
+
* - **`create(message, options)`** ← Lower-level
|
|
761
|
+
* - Creates NEW instance from KNOWN message + options
|
|
762
|
+
* - Simple: `new this(message, options)`
|
|
763
|
+
* - Override for: post-construction logic only
|
|
764
|
+
*
|
|
765
|
+
* - **`withOptions(exception, options)`** ← Different purpose
|
|
766
|
+
* - Mutates EXISTING instance (no creation)
|
|
767
|
+
* - Just updates properties
|
|
768
|
+
* - Use for: modifying already-created exceptions
|
|
769
|
+
*
|
|
770
|
+
* **Flow in action**:
|
|
771
|
+
* ```typescript
|
|
772
|
+
* // User calls:
|
|
773
|
+
* BaseException.from(unknownError)
|
|
774
|
+
* ↓
|
|
775
|
+
* // Internally:
|
|
776
|
+
* createFromError(unknownError) // ← YOU ARE HERE (detect & extract)
|
|
777
|
+
* ↓
|
|
778
|
+
* parseErrorMessage(unknownError) // Extract message
|
|
779
|
+
* parseErrorDetails(unknownError) // Extract details
|
|
780
|
+
* ↓
|
|
781
|
+
* create(message, options) // Create new instance
|
|
782
|
+
* ↓
|
|
783
|
+
* new this(message, options) // Constructor
|
|
784
|
+
* ```
|
|
785
|
+
*
|
|
786
|
+
* **Implementation tips**:
|
|
787
|
+
* - Always fall back to `super.createFromError()` for unknown error types
|
|
788
|
+
* - Preserve the original error as `cause` for debugging
|
|
789
|
+
* - Use type guards to safely check error properties
|
|
790
|
+
* - Return specific status codes (400 for client errors, 500 for server errors)
|
|
791
|
+
* - Include relevant context in details for troubleshooting
|
|
792
|
+
*
|
|
793
|
+
* **Method Comparison Example**:
|
|
794
|
+
* ```typescript
|
|
795
|
+
* const dbError = { code: '23505', message: 'Duplicate key' };
|
|
796
|
+
*
|
|
797
|
+
* // createFromError: DETECTS + EXTRACTS + CREATES
|
|
798
|
+
* const ex1 = createFromError(dbError, {});
|
|
799
|
+
* // Detects code 23505 → sets code='DB_DUPLICATE', status=409, etc.
|
|
800
|
+
*
|
|
801
|
+
* // create: Just CREATES (you provide everything)
|
|
802
|
+
* const ex2 = create('Duplicate key', { code: 'DB_DUPLICATE', statusCode: 409 });
|
|
803
|
+
* // No detection, you already know what to set
|
|
804
|
+
*
|
|
805
|
+
* // withOptions: MUTATES existing
|
|
806
|
+
* const ex3 = withOptions(ex2, { code: 'NEW_CODE' });
|
|
807
|
+
* // ex3 === ex2 (same instance, just code changed)
|
|
808
|
+
* ```
|
|
809
|
+
*
|
|
810
|
+
* @see {@link from} - Public method that calls this after handling JSON/instances
|
|
811
|
+
* @see {@link create} - Lower-level method for creating instances
|
|
812
|
+
* @see {@link parseErrorMessage} - Override for custom message extraction only
|
|
813
|
+
* @see {@link parseErrorDetails} - Override for custom detail extraction only
|
|
814
|
+
*/
|
|
815
|
+
protected static createFromError<TDetails = unknown, TException extends BaseException<TDetails> = BaseException<TDetails>, TCause = unknown>(this: BaseExceptionConstructor<TDetails, TException>, error: unknown, options?: BaseExceptionOptions<TDetails, TCause>): TException;
|
|
816
|
+
static isValidationError(error: unknown): error is ValidatorError;
|
|
817
|
+
static getValidationError(error: unknown): ValidatorError | ValidatorClassError | ValidatorBulkError | null;
|
|
818
|
+
/**
|
|
819
|
+
* Protected method to extract a human-readable message from an error value.
|
|
820
|
+
*
|
|
821
|
+
* Override this in subclasses to customize how messages are extracted from different
|
|
822
|
+
* error types. For example, you might want to format validation errors differently
|
|
823
|
+
* or add context from error properties.
|
|
824
|
+
*
|
|
825
|
+
* The default implementation:
|
|
826
|
+
* 1. Extracts `message` property from objects
|
|
827
|
+
* 2. Uses the string value directly if error is a string
|
|
828
|
+
* 3. Falls back to `Error.message` for Error instances
|
|
829
|
+
* 4. Uses `options.fallbackMessage` if provided
|
|
830
|
+
* 5. Defaults to "Unknown Error" if nothing else works
|
|
831
|
+
*
|
|
832
|
+
* @param error - The error value to extract a message from
|
|
833
|
+
* @param options - Optional configuration with fallbackMessage
|
|
834
|
+
* @returns The extracted error message string
|
|
835
|
+
*
|
|
836
|
+
* @example
|
|
837
|
+
* ```typescript
|
|
838
|
+
* // Custom validation exception with formatted message
|
|
839
|
+
* class ValidationException extends BaseException {
|
|
840
|
+
* protected static override parseErrorMessage(
|
|
841
|
+
* error: unknown,
|
|
842
|
+
* options?: BaseExceptionOptions
|
|
843
|
+
* ): string {
|
|
844
|
+
* if (typeof error === 'object' && error !== null) {
|
|
845
|
+
* const err = error as Record<string, unknown>;
|
|
846
|
+
*
|
|
847
|
+
* // Check for validation error array
|
|
848
|
+
* if (Array.isArray(err.errors) && err.errors.length > 0) {
|
|
849
|
+
* const firstError = err.errors[0];
|
|
850
|
+
* if (typeof firstError === 'object' && firstError !== null) {
|
|
851
|
+
* const field = (firstError as any).field;
|
|
852
|
+
* const msg = (firstError as any).message;
|
|
853
|
+
* return `Validation failed for ${field}: ${msg}`;
|
|
854
|
+
* }
|
|
855
|
+
* }
|
|
856
|
+
* }
|
|
857
|
+
*
|
|
858
|
+
* // Fall back to default
|
|
859
|
+
* return super.parseErrorMessage(error, options);
|
|
860
|
+
* }
|
|
861
|
+
* }
|
|
862
|
+
*
|
|
863
|
+
* const validationError = {
|
|
864
|
+
* errors: [{ field: 'email', message: 'Invalid format' }]
|
|
865
|
+
* };
|
|
866
|
+
* const ex = ValidationException.from(validationError);
|
|
867
|
+
* // ex.message === 'Validation failed for email: Invalid format'
|
|
868
|
+
* ```
|
|
869
|
+
*
|
|
870
|
+
* @example
|
|
871
|
+
* ```typescript
|
|
872
|
+
* // Add table context to database errors
|
|
873
|
+
* class DatabaseException extends BaseException {
|
|
874
|
+
* protected static override parseErrorMessage(
|
|
875
|
+
* error: unknown,
|
|
876
|
+
* options?: BaseExceptionOptions
|
|
877
|
+
* ): string {
|
|
878
|
+
* const baseMessage = super.parseErrorMessage(error, options);
|
|
879
|
+
*
|
|
880
|
+
* if (typeof error === 'object' && error !== null) {
|
|
881
|
+
* const err = error as Record<string, unknown>;
|
|
882
|
+
* if (typeof err.table === 'string') {
|
|
883
|
+
* return `${baseMessage} (table: ${err.table})`;
|
|
884
|
+
* }
|
|
885
|
+
* }
|
|
886
|
+
*
|
|
887
|
+
* return baseMessage;
|
|
888
|
+
* }
|
|
889
|
+
* }
|
|
890
|
+
* ```
|
|
891
|
+
*
|
|
892
|
+
* @remarks
|
|
893
|
+
* - Always returns a non-empty string (minimum "Unknown Error")
|
|
894
|
+
* - Sanitizes "undefined" strings to "Unknown Error"
|
|
895
|
+
* - Called by `createFromError()` during error conversion
|
|
896
|
+
*/
|
|
897
|
+
protected static parseErrorMessage(error: unknown, options?: BaseExceptionOptions): string;
|
|
898
|
+
/**
|
|
899
|
+
* Protected method to extract structured details from an error value.
|
|
900
|
+
*
|
|
901
|
+
* Override this in subclasses to extract domain-specific fields from errors.
|
|
902
|
+
* For example, API errors might have `endpoint` and `method`, while database
|
|
903
|
+
* errors might have `query` and `table`.
|
|
904
|
+
*
|
|
905
|
+
* The default implementation:
|
|
906
|
+
* 1. Spreads all properties from the error object (if it's an object)
|
|
907
|
+
* 2. Merges with `options.details` (options take precedence)
|
|
908
|
+
*
|
|
909
|
+
* @template TDetails - Type of the details object
|
|
910
|
+
* @template TCause - Type of the cause object
|
|
911
|
+
* @param error - The error value to extract details from
|
|
912
|
+
* @param options - Optional configuration with details to merge
|
|
913
|
+
* @returns Object containing extracted details
|
|
914
|
+
*
|
|
915
|
+
* @example
|
|
916
|
+
* ```typescript
|
|
917
|
+
* // Extract API-specific fields
|
|
918
|
+
* interface ApiErrorDetails {
|
|
919
|
+
* endpoint?: string;
|
|
920
|
+
* method?: string;
|
|
921
|
+
* requestId?: string;
|
|
922
|
+
* }
|
|
923
|
+
*
|
|
924
|
+
* class ApiException extends BaseException<ApiErrorDetails> {
|
|
925
|
+
* protected static override parseErrorDetails<
|
|
926
|
+
* TDetails = ApiErrorDetails
|
|
927
|
+
* >(
|
|
928
|
+
* error: unknown,
|
|
929
|
+
* options?: BaseExceptionOptions<TDetails>
|
|
930
|
+
* ) {
|
|
931
|
+
* const baseDetails = super.parseErrorDetails(error, options);
|
|
932
|
+
*
|
|
933
|
+
* if (typeof error === 'object' && error !== null) {
|
|
934
|
+
* const err = error as Record<string, unknown>;
|
|
935
|
+
* return {
|
|
936
|
+
* ...baseDetails,
|
|
937
|
+
* endpoint: typeof err.url === 'string' ? err.url : undefined,
|
|
938
|
+
* method: typeof err.method === 'string' ? err.method : undefined,
|
|
939
|
+
* requestId: typeof err.requestId === 'string' ? err.requestId : undefined
|
|
940
|
+
* };
|
|
941
|
+
* }
|
|
942
|
+
*
|
|
943
|
+
* return baseDetails;
|
|
944
|
+
* }
|
|
945
|
+
* }
|
|
946
|
+
* ```
|
|
947
|
+
*
|
|
948
|
+
* @example
|
|
949
|
+
* ```typescript
|
|
950
|
+
* // Extract database-specific fields
|
|
951
|
+
* interface DbErrorDetails {
|
|
952
|
+
* query?: string;
|
|
953
|
+
* table?: string;
|
|
954
|
+
* constraint?: string;
|
|
955
|
+
* }
|
|
956
|
+
*
|
|
957
|
+
* class DatabaseException extends BaseException<DbErrorDetails> {
|
|
958
|
+
* protected static override parseErrorDetails<
|
|
959
|
+
* TDetails = DbErrorDetails
|
|
960
|
+
* >(
|
|
961
|
+
* error: unknown,
|
|
962
|
+
* options?: BaseExceptionOptions<TDetails>
|
|
963
|
+
* ) {
|
|
964
|
+
* const baseDetails = super.parseErrorDetails(error, options);
|
|
965
|
+
*
|
|
966
|
+
* if (typeof error === 'object' && error !== null) {
|
|
967
|
+
* const err = error as Record<string, unknown>;
|
|
968
|
+
* return {
|
|
969
|
+
* ...baseDetails,
|
|
970
|
+
* query: typeof err.sql === 'string' ? err.sql : undefined,
|
|
971
|
+
* table: typeof err.table === 'string' ? err.table : undefined,
|
|
972
|
+
* constraint: typeof err.constraint === 'string' ? err.constraint : undefined
|
|
973
|
+
* };
|
|
974
|
+
* }
|
|
975
|
+
*
|
|
976
|
+
* return baseDetails;
|
|
977
|
+
* }
|
|
978
|
+
* }
|
|
979
|
+
* ```
|
|
980
|
+
*
|
|
981
|
+
* @remarks
|
|
982
|
+
* - Returns all properties from error objects by default
|
|
983
|
+
* - Options.details take precedence over extracted details
|
|
984
|
+
* - Called by `createFromError()` during error conversion
|
|
985
|
+
* - Use TypeScript interfaces to ensure type safety of extracted fields
|
|
986
|
+
*/
|
|
987
|
+
protected static parseErrorDetails<TDetails = unknown, TCause = unknown>(error: unknown, options?: BaseExceptionOptions<TDetails, TCause>): TDetails;
|
|
988
|
+
/**
|
|
989
|
+
* Merges additional options into an existing BaseException instance.
|
|
990
|
+
*
|
|
991
|
+
* This utility method mutates the exception instance by updating its `code`,
|
|
992
|
+
* `statusCode`, and `details` properties with values from the options object.
|
|
993
|
+
* Used internally by `from()` when reusing existing exceptions.
|
|
994
|
+
*
|
|
995
|
+
* @template TException - The exception type (automatically inferred)
|
|
996
|
+
* @param error - The exception instance to update
|
|
997
|
+
* @param options - Options to merge into the exception
|
|
998
|
+
* @returns The same exception instance with updated properties
|
|
999
|
+
*
|
|
1000
|
+
* @example
|
|
1001
|
+
* ```typescript
|
|
1002
|
+
* // Update error code
|
|
1003
|
+
* const error = new BaseException('Failed');
|
|
1004
|
+
* BaseException.withOptions(error, { code: 'NEW_CODE' });
|
|
1005
|
+
* console.log(error.code); // 'NEW_CODE'
|
|
1006
|
+
* ```
|
|
1007
|
+
*
|
|
1008
|
+
* @example
|
|
1009
|
+
* ```typescript
|
|
1010
|
+
* // Merge details
|
|
1011
|
+
* const error = new BaseException('Error', {
|
|
1012
|
+
* details: { originalData: 'value1' }
|
|
1013
|
+
* });
|
|
1014
|
+
*
|
|
1015
|
+
* BaseException.withOptions(error, {
|
|
1016
|
+
* details: { additionalData: 'value2' }
|
|
1017
|
+
* });
|
|
1018
|
+
*
|
|
1019
|
+
* console.log(error.details);
|
|
1020
|
+
* // { originalData: 'value1', additionalData: 'value2' }
|
|
1021
|
+
* ```
|
|
1022
|
+
*
|
|
1023
|
+
* @example
|
|
1024
|
+
* ```typescript
|
|
1025
|
+
* // Update multiple properties
|
|
1026
|
+
* const error = new BaseException('Error');
|
|
1027
|
+
* BaseException.withOptions(error, {
|
|
1028
|
+
* code: 'UPDATED_CODE',
|
|
1029
|
+
* statusCode: 503,
|
|
1030
|
+
* details: { retry: true }
|
|
1031
|
+
* });
|
|
1032
|
+
* ```
|
|
1033
|
+
*
|
|
1034
|
+
* @remarks
|
|
1035
|
+
* **IMPORTANT: This method MUTATES the input exception instance.**
|
|
1036
|
+
* Unlike `create()` and `createFromError()` which create NEW instances,
|
|
1037
|
+
* `withOptions()` modifies the existing exception in-place for performance.
|
|
1038
|
+
*
|
|
1039
|
+
* **Key Differences from Related Methods**:
|
|
1040
|
+
* - **`withOptions(exception, options)`** ← YOU ARE HERE
|
|
1041
|
+
* - **MUTATES** existing instance (no new instance created)
|
|
1042
|
+
* - Just updates: `error.code = ...; error.statusCode = ...`
|
|
1043
|
+
* - Use for: merging options into already-created exceptions
|
|
1044
|
+
* - Returns: THE SAME instance (not a copy)
|
|
1045
|
+
*
|
|
1046
|
+
* - **`create(message, options)`** ← Creates NEW
|
|
1047
|
+
* - Creates NEW instance from known message + options
|
|
1048
|
+
* - Simple: `new this(message, options)`
|
|
1049
|
+
* - Returns: NEW exception instance
|
|
1050
|
+
*
|
|
1051
|
+
* - **`createFromError(error, options)`** ← Converts + Creates NEW
|
|
1052
|
+
* - Converts unknown error → extracts data → creates NEW instance
|
|
1053
|
+
* - Complex: detects, parses, then creates
|
|
1054
|
+
* - Returns: NEW exception instance
|
|
1055
|
+
*
|
|
1056
|
+
* **When this is called**:
|
|
1057
|
+
* - Automatically by `from()` when the error is already an instance of the target class
|
|
1058
|
+
* - For merging additional options into an existing exception without recreating it
|
|
1059
|
+
*
|
|
1060
|
+
* **Method Comparison Example**:
|
|
1061
|
+
* ```typescript
|
|
1062
|
+
* const original = new BaseException('Error', { code: 'ORIG' });
|
|
1063
|
+
*
|
|
1064
|
+
* // withOptions: MUTATES (same instance)
|
|
1065
|
+
* const updated = BaseException.withOptions(original, { code: 'NEW' });
|
|
1066
|
+
* console.log(updated === original); // true (same instance!)
|
|
1067
|
+
* console.log(original.code); // 'NEW' (original was mutated)
|
|
1068
|
+
*
|
|
1069
|
+
* // create: NEW instance
|
|
1070
|
+
* const created = BaseException.create('Error', { code: 'CREATED' });
|
|
1071
|
+
* console.log(created === original); // false (different instance)
|
|
1072
|
+
*
|
|
1073
|
+
* // createFromError: NEW instance from error
|
|
1074
|
+
* const converted = BaseException.createFromError(someError, {});
|
|
1075
|
+
* console.log(converted === original); // false (different instance)
|
|
1076
|
+
* ```
|
|
1077
|
+
*
|
|
1078
|
+
* **Performance reasoning**:
|
|
1079
|
+
* Mutation is intentional to avoid unnecessary object creation when reusing
|
|
1080
|
+
* exceptions via `from()`. If you need a copy, create a new instance instead.
|
|
1081
|
+
*
|
|
1082
|
+
* @see {@link from} - Calls this when error is already an instance of target class
|
|
1083
|
+
* @see {@link create} - Creates new instances (doesn't mutate)
|
|
1084
|
+
* @see {@link createFromError} - Converts errors to new instances (doesn't mutate)
|
|
1085
|
+
*/
|
|
1086
|
+
static withOptions<TException extends BaseException>(error: TException, options?: BaseExceptionOptions): TException;
|
|
1087
|
+
/**
|
|
1088
|
+
* Wraps an async operation, converting any thrown errors into BaseException.
|
|
1089
|
+
*
|
|
1090
|
+
* Executes the operation and returns its result if successful. If it throws,
|
|
1091
|
+
* converts the error to a BaseException using `from()` and **RE-THROWS** it.
|
|
1092
|
+
*
|
|
1093
|
+
* **Error Handling Pattern**: Traditional exception throwing (try/catch pattern)
|
|
1094
|
+
*
|
|
1095
|
+
* **Key Differences from Related Methods**:
|
|
1096
|
+
* - **`wrap(operation, options)`** ← YOU ARE HERE
|
|
1097
|
+
* - **ASYNC ONLY** (returns Promise)
|
|
1098
|
+
* - **THROWS** BaseException on error (traditional exception pattern)
|
|
1099
|
+
* - Use when: You want exceptions to bubble up to error handlers
|
|
1100
|
+
* - Pattern: try/catch or framework error handlers catch it
|
|
1101
|
+
*
|
|
1102
|
+
* - **`tryCatch(operation, options)`** ← Returns errors instead of throwing
|
|
1103
|
+
* - **ASYNC** (returns Promise<[error, null] | [null, result]>)
|
|
1104
|
+
* - **RETURNS** error tuple (Result/Either pattern, Go-style)
|
|
1105
|
+
* - Use when: You want explicit error handling without try/catch
|
|
1106
|
+
* - Pattern: `const [error, result] = await tryCatch(...)`
|
|
1107
|
+
*
|
|
1108
|
+
* - **`tryCatchSync(operation, options)`** ← Sync version of tryCatch
|
|
1109
|
+
* - **SYNC** (returns [error, null] | [null, result])
|
|
1110
|
+
* - **RETURNS** error tuple (Result/Either pattern)
|
|
1111
|
+
* - Use when: Synchronous operations, prefer explicit errors
|
|
1112
|
+
* - Pattern: `const [error, result] = tryCatchSync(...)`
|
|
1113
|
+
*
|
|
1114
|
+
* @template TResult - The return type of the operation
|
|
1115
|
+
* @template TDetails - Type of exception details
|
|
1116
|
+
* @template TCause - Type of the cause
|
|
1117
|
+
* @param operation - Async function to execute
|
|
1118
|
+
* @param options - Optional exception metadata to add if an error occurs
|
|
1119
|
+
* @returns Promise resolving to the operation result
|
|
1120
|
+
* @throws {BaseException} If the operation throws (converted from original error)
|
|
1121
|
+
*
|
|
1122
|
+
* @example
|
|
1123
|
+
* ```typescript
|
|
1124
|
+
* // Wrap API call
|
|
1125
|
+
* const data = await BaseException.wrap(
|
|
1126
|
+
* async () => fetch('/api/users').then(r => r.json()),
|
|
1127
|
+
* { code: 'API_ERROR', statusCode: 503 }
|
|
1128
|
+
* );
|
|
1129
|
+
* // On error, throws BaseException with code 'API_ERROR'
|
|
1130
|
+
* ```
|
|
1131
|
+
*
|
|
1132
|
+
* @example
|
|
1133
|
+
* ```typescript
|
|
1134
|
+
* // Wrap database query
|
|
1135
|
+
* async function getUser(id: number) {
|
|
1136
|
+
* return BaseException.wrap(
|
|
1137
|
+
* async () => db.query('SELECT * FROM users WHERE id = ?', [id]),
|
|
1138
|
+
* {
|
|
1139
|
+
* code: 'DB_ERROR',
|
|
1140
|
+
* statusCode: 500,
|
|
1141
|
+
* details: { query: 'getUser', userId: id }
|
|
1142
|
+
* }
|
|
1143
|
+
* );
|
|
1144
|
+
* }
|
|
1145
|
+
* ```
|
|
1146
|
+
*
|
|
1147
|
+
* @example
|
|
1148
|
+
* ```typescript
|
|
1149
|
+
* // Chain wrapping
|
|
1150
|
+
* const result = await BaseException.wrap(
|
|
1151
|
+
* async () => {
|
|
1152
|
+
* const user = await getUser(123);
|
|
1153
|
+
* const orders = await getOrders(user.id);
|
|
1154
|
+
* return { user, orders };
|
|
1155
|
+
* },
|
|
1156
|
+
* { code: 'USER_ORDERS_ERROR' }
|
|
1157
|
+
* );
|
|
1158
|
+
* ```
|
|
1159
|
+
*
|
|
1160
|
+
* @remarks
|
|
1161
|
+
* **Error Handling Pattern**: THROWS exceptions (traditional)
|
|
1162
|
+
* - Errors bubble up to framework error handlers or try/catch blocks
|
|
1163
|
+
* - Good for: Express/NestJS middleware, API routes, service layers
|
|
1164
|
+
* - Clean syntax when you don't need inline error handling
|
|
1165
|
+
*
|
|
1166
|
+
* **When to use `wrap()` vs `tryCatch()`**:
|
|
1167
|
+
* - Use `wrap()` when exceptions should bubble up (APIs, middleware)
|
|
1168
|
+
* - Use `tryCatch()` when you need explicit error handling inline
|
|
1169
|
+
*
|
|
1170
|
+
* **Comparison Example**:
|
|
1171
|
+
* ```typescript
|
|
1172
|
+
* // wrap: THROWS (traditional exception)
|
|
1173
|
+
* async function fetchUser1(id: number) {
|
|
1174
|
+
* return BaseException.wrap(
|
|
1175
|
+
* async () => db.users.findById(id),
|
|
1176
|
+
* { code: 'USER_FETCH_ERROR' }
|
|
1177
|
+
* );
|
|
1178
|
+
* // On error: throws BaseException, caller must try/catch
|
|
1179
|
+
* }
|
|
1180
|
+
*
|
|
1181
|
+
* // tryCatch: RETURNS error (Result pattern)
|
|
1182
|
+
* async function fetchUser2(id: number) {
|
|
1183
|
+
* const [error, user] = await BaseException.tryCatch(
|
|
1184
|
+
* async () => db.users.findById(id),
|
|
1185
|
+
* { code: 'USER_FETCH_ERROR' }
|
|
1186
|
+
* );
|
|
1187
|
+
* if (error) return null; // Handle inline
|
|
1188
|
+
* return user;
|
|
1189
|
+
* }
|
|
1190
|
+
*
|
|
1191
|
+
* // Usage:
|
|
1192
|
+
* try {
|
|
1193
|
+
* const user = await fetchUser1(123); // May throw
|
|
1194
|
+
* } catch (e) {
|
|
1195
|
+
* // Handle error here
|
|
1196
|
+
* }
|
|
1197
|
+
*
|
|
1198
|
+
* const user = await fetchUser2(123); // Never throws, returns null on error
|
|
1199
|
+
* ```
|
|
1200
|
+
*
|
|
1201
|
+
* @see {@link tryCatch} - Async version that returns error tuples instead of throwing
|
|
1202
|
+
* @see {@link tryCatchSync} - Sync version that returns error tuples
|
|
1203
|
+
* @see {@link from} - The conversion method used internally
|
|
1204
|
+
*/
|
|
1205
|
+
static wrap<TResult, TDetails = unknown, TCause = unknown>(operation: () => Promise<TResult>, options?: BaseExceptionOptions<TDetails, TCause>): Promise<TResult>;
|
|
1206
|
+
/**
|
|
1207
|
+
* Immediately throws a BaseException created from the given error value.
|
|
1208
|
+
*
|
|
1209
|
+
* This is a convenience method equivalent to `throw BaseException.from(error, options)`.
|
|
1210
|
+
* Useful for concise one-liner error throwing with proper conversion.
|
|
1211
|
+
*
|
|
1212
|
+
* @template TDetails - Type of exception details
|
|
1213
|
+
* @template TCause - Type of exception cause
|
|
1214
|
+
* @param error - The error value to convert and throw
|
|
1215
|
+
* @param options - Optional exception metadata
|
|
1216
|
+
* @throws {BaseException} Always throws
|
|
1217
|
+
*
|
|
1218
|
+
* @example
|
|
1219
|
+
* ```typescript
|
|
1220
|
+
* // One-liner error throwing
|
|
1221
|
+
* if (!user) {
|
|
1222
|
+
* BaseException.throw('User not found', {
|
|
1223
|
+
* code: 'USER_NOT_FOUND',
|
|
1224
|
+
* statusCode: 404
|
|
1225
|
+
* });
|
|
1226
|
+
* }
|
|
1227
|
+
* ```
|
|
1228
|
+
*
|
|
1229
|
+
* @example
|
|
1230
|
+
* ```typescript
|
|
1231
|
+
* // Convert and re-throw
|
|
1232
|
+
* try {
|
|
1233
|
+
* riskyOperation();
|
|
1234
|
+
* } catch (err) {
|
|
1235
|
+
* BaseException.throw(err, {
|
|
1236
|
+
* code: 'OPERATION_FAILED',
|
|
1237
|
+
* details: { context: 'important data' }
|
|
1238
|
+
* });
|
|
1239
|
+
* }
|
|
1240
|
+
* ```
|
|
1241
|
+
*
|
|
1242
|
+
* @example
|
|
1243
|
+
* ```typescript
|
|
1244
|
+
* // Conditional throwing
|
|
1245
|
+
* const validateAge = (age: number) => {
|
|
1246
|
+
* age < 0 && BaseException.throw('Invalid age', { code: 'VALIDATION_ERROR' });
|
|
1247
|
+
* age > 150 && BaseException.throw('Age too high', { code: 'VALIDATION_ERROR' });
|
|
1248
|
+
* return age;
|
|
1249
|
+
* };
|
|
1250
|
+
* ```
|
|
1251
|
+
*
|
|
1252
|
+
* @remarks
|
|
1253
|
+
* Return type is `never` since this function always throws.
|
|
1254
|
+
* Helps TypeScript understand control flow correctly.
|
|
1255
|
+
*
|
|
1256
|
+
* @see {@link from} - Creates exception without throwing
|
|
1257
|
+
*/
|
|
1258
|
+
static throw<TDetails = unknown, TCause = unknown>(error: unknown, options?: BaseExceptionOptions<TDetails, TCause>): never;
|
|
1259
|
+
/**
|
|
1260
|
+
* Safely executes a synchronous operation and returns a Result tuple.
|
|
1261
|
+
*
|
|
1262
|
+
* This method implements the **Result/Either pattern**, returning a tuple of
|
|
1263
|
+
* `[error, null]` if the operation fails, or `[null, result]` if it succeeds.
|
|
1264
|
+
* This eliminates the need for try-catch blocks and makes error handling explicit.
|
|
1265
|
+
*
|
|
1266
|
+
* **Error Handling Pattern**: Returns error tuples (Go/Rust-style)
|
|
1267
|
+
*
|
|
1268
|
+
* **Key Differences from Related Methods**:
|
|
1269
|
+
* - **`tryCatchSync(operation, options)`** ← YOU ARE HERE
|
|
1270
|
+
* - **SYNC** (returns immediately)
|
|
1271
|
+
* - **RETURNS** error tuple: `[error, null] | [null, result]`
|
|
1272
|
+
* - Use when: Synchronous operations, explicit error handling
|
|
1273
|
+
* - Pattern: `const [error, result] = tryCatchSync(...)`
|
|
1274
|
+
*
|
|
1275
|
+
* - **`tryCatch(operation, options)`** ← Async version
|
|
1276
|
+
* - **ASYNC** (returns Promise<[error, null] | [null, result]>)
|
|
1277
|
+
* - **RETURNS** error tuple (same pattern, but async)
|
|
1278
|
+
* - Use when: Async operations, explicit error handling
|
|
1279
|
+
* - Pattern: `const [error, result] = await tryCatch(...)`
|
|
1280
|
+
*
|
|
1281
|
+
* - **`wrap(operation, options)`** ← Different error pattern
|
|
1282
|
+
* - **ASYNC ONLY**
|
|
1283
|
+
* - **THROWS** BaseException on error (traditional exceptions)
|
|
1284
|
+
* - Use when: You want errors to bubble up to handlers
|
|
1285
|
+
* - Pattern: `try { await wrap(...) } catch (e) { ... }`
|
|
1286
|
+
*
|
|
1287
|
+
* **Type-safe**: When called on subclasses, returns that subclass type in the error position.
|
|
1288
|
+
*
|
|
1289
|
+
* @template TResult - The return type of the operation
|
|
1290
|
+
* @template TDetails - Type of exception details
|
|
1291
|
+
* @template TCause - Type of exception cause
|
|
1292
|
+
* @param this - The exception constructor (auto-bound)
|
|
1293
|
+
* @param operation - Synchronous function to execute
|
|
1294
|
+
* @param options - Optional exception metadata if an error occurs
|
|
1295
|
+
* @returns Tuple of `[error, null]` or `[null, result]` (never throws)
|
|
1296
|
+
*
|
|
1297
|
+
* @example
|
|
1298
|
+
* ```typescript
|
|
1299
|
+
* // Basic usage
|
|
1300
|
+
* const [error, result] = BaseException.tryCatchSync(() => {
|
|
1301
|
+
* return JSON.parse(someString);
|
|
1302
|
+
* });
|
|
1303
|
+
*
|
|
1304
|
+
* if (error) {
|
|
1305
|
+
* console.error('Parse failed:', error.message);
|
|
1306
|
+
* return;
|
|
1307
|
+
* }
|
|
1308
|
+
*
|
|
1309
|
+
* console.log('Parsed:', result);
|
|
1310
|
+
* ```
|
|
1311
|
+
*
|
|
1312
|
+
* @example
|
|
1313
|
+
* ```typescript
|
|
1314
|
+
* // With additional error metadata
|
|
1315
|
+
* const [error, user] = BaseException.tryCatchSync(
|
|
1316
|
+
* () => validateUser(data),
|
|
1317
|
+
* {
|
|
1318
|
+
* code: 'VALIDATION_ERROR',
|
|
1319
|
+
* statusCode: 400,
|
|
1320
|
+
* details: { input: data }
|
|
1321
|
+
* }
|
|
1322
|
+
* );
|
|
1323
|
+
* ```
|
|
1324
|
+
*
|
|
1325
|
+
* @example
|
|
1326
|
+
* ```typescript
|
|
1327
|
+
* // Chaining operations
|
|
1328
|
+
* function processData(input: string) {
|
|
1329
|
+
* const [parseError, data] = BaseException.tryCatchSync(() =>
|
|
1330
|
+
* JSON.parse(input)
|
|
1331
|
+
* );
|
|
1332
|
+
* if (parseError) return { error: parseError };
|
|
1333
|
+
*
|
|
1334
|
+
* const [validError, validated] = BaseException.tryCatchSync(() =>
|
|
1335
|
+
* validateData(data)
|
|
1336
|
+
* );
|
|
1337
|
+
* if (validError) return { error: validError };
|
|
1338
|
+
*
|
|
1339
|
+
* return { data: validated };
|
|
1340
|
+
* }
|
|
1341
|
+
* ```
|
|
1342
|
+
*
|
|
1343
|
+
* @example
|
|
1344
|
+
* ```typescript
|
|
1345
|
+
* // With subclass - maintains type
|
|
1346
|
+
* class ValidationException extends BaseException {}
|
|
1347
|
+
*
|
|
1348
|
+
* const [error, result] = ValidationException.tryCatchSync(() =>
|
|
1349
|
+
* validateEmail(email)
|
|
1350
|
+
* );
|
|
1351
|
+
*
|
|
1352
|
+
* if (error) {
|
|
1353
|
+
* // error is typed as ValidationException
|
|
1354
|
+
* console.log(error.name); // 'ValidationException'
|
|
1355
|
+
* }
|
|
1356
|
+
* ```
|
|
1357
|
+
*
|
|
1358
|
+
* @example
|
|
1359
|
+
* ```typescript
|
|
1360
|
+
* // Functional programming style
|
|
1361
|
+
* const results = data.map(item =>
|
|
1362
|
+
* BaseException.tryCatchSync(() => processItem(item))
|
|
1363
|
+
* );
|
|
1364
|
+
*
|
|
1365
|
+
* const errors = results
|
|
1366
|
+
* .filter(([err]) => err !== null)
|
|
1367
|
+
* .map(([err]) => err);
|
|
1368
|
+
*
|
|
1369
|
+
* const values = results
|
|
1370
|
+
* .filter(([, val]) => val !== null)
|
|
1371
|
+
* .map(([, val]) => val);
|
|
1372
|
+
* ```
|
|
1373
|
+
*
|
|
1374
|
+
* @remarks
|
|
1375
|
+
* **Result Pattern**: Returns `[error, null] | [null, result]` (NEVER THROWS)
|
|
1376
|
+
* - Eliminates try-catch blocks
|
|
1377
|
+
* - Makes error handling explicit and type-safe
|
|
1378
|
+
* - Works well with destructuring
|
|
1379
|
+
* - Inspired by Go's error handling
|
|
1380
|
+
*
|
|
1381
|
+
* **When to use `tryCatchSync()` vs `wrap()` vs `tryCatch()`**:
|
|
1382
|
+
* - Use `tryCatchSync()`: SYNC operations, prefer explicit errors (no exceptions)
|
|
1383
|
+
* - Use `tryCatch()`: ASYNC operations, prefer explicit errors (no exceptions)
|
|
1384
|
+
* - Use `wrap()`: ASYNC operations, want traditional exceptions to bubble up
|
|
1385
|
+
*
|
|
1386
|
+
* **Pattern Comparison**:
|
|
1387
|
+
* ```typescript
|
|
1388
|
+
* // tryCatchSync: SYNC + RETURNS error
|
|
1389
|
+
* const [error, result] = BaseException.tryCatchSync(() =>
|
|
1390
|
+
* JSON.parse(jsonString)
|
|
1391
|
+
* );
|
|
1392
|
+
* if (error) {
|
|
1393
|
+
* console.error('Parse failed:', error.message);
|
|
1394
|
+
* return defaultValue;
|
|
1395
|
+
* }
|
|
1396
|
+
* return result;
|
|
1397
|
+
*
|
|
1398
|
+
* // tryCatch: ASYNC + RETURNS error
|
|
1399
|
+
* const [error, data] = await BaseException.tryCatch(async () =>
|
|
1400
|
+
* fetch('/api/data').then(r => r.json())
|
|
1401
|
+
* );
|
|
1402
|
+
* if (error) return handleError(error);
|
|
1403
|
+
* return processData(data);
|
|
1404
|
+
*
|
|
1405
|
+
* // wrap: ASYNC + THROWS error
|
|
1406
|
+
* try {
|
|
1407
|
+
* const data = await BaseException.wrap(
|
|
1408
|
+
* async () => fetch('/api/data').then(r => r.json())
|
|
1409
|
+
* );
|
|
1410
|
+
* return processData(data);
|
|
1411
|
+
* } catch (error) {
|
|
1412
|
+
* return handleError(error); // Must use try/catch
|
|
1413
|
+
* }
|
|
1414
|
+
* ```
|
|
1415
|
+
*
|
|
1416
|
+
* **Advantages over try/catch**:
|
|
1417
|
+
* - No nested blocks (flatter code)
|
|
1418
|
+
* - Error variable is properly scoped
|
|
1419
|
+
* - TypeScript narrows types correctly
|
|
1420
|
+
* - Works great with early returns
|
|
1421
|
+
* - Composable (map over arrays, etc.)
|
|
1422
|
+
*
|
|
1423
|
+
* @see {@link tryCatch} - Async version
|
|
1424
|
+
* @see {@link wrap} - Async version that throws instead of returning tuple
|
|
1425
|
+
*/
|
|
1426
|
+
static tryCatchSync<TResult, TDetails = unknown, TCause = unknown>(this: BaseExceptionConstructor<TDetails, BaseException<TDetails>>, operation: () => TResult, options?: BaseExceptionOptions<TDetails, TCause>): [BaseException<TDetails>, null] | [null, TResult];
|
|
1427
|
+
/**
|
|
1428
|
+
* Safely executes an async operation and returns a Result tuple Promise.
|
|
1429
|
+
*
|
|
1430
|
+
* Async version of `tryCatchSync`. Returns a Promise that resolves to a tuple of
|
|
1431
|
+
* `[error, null]` if the operation fails, or `[null, result]` if it succeeds.\
|
|
1432
|
+
* Eliminates async try-catch blocks and makes async error handling explicit.
|
|
1433
|
+
*
|
|
1434
|
+
* **Error Handling Pattern**: Returns error tuples (Go/Rust-style)
|
|
1435
|
+
*
|
|
1436
|
+
* **Key Differences from Related Methods**:
|
|
1437
|
+
* - **`tryCatch(operation, options)`** ← YOU ARE HERE
|
|
1438
|
+
* - **ASYNC** (returns Promise<[error, null] | [null, result]>)
|
|
1439
|
+
* - **RETURNS** error tuple (Result/Either pattern)
|
|
1440
|
+
* - Use when: Async operations, explicit error handling
|
|
1441
|
+
* - Pattern: `const [error, result] = await tryCatch(...)`
|
|
1442
|
+
*
|
|
1443
|
+
* - **`tryCatchSync(operation, options)`** ← Sync version
|
|
1444
|
+
* - **SYNC** (returns [error, null] | [null, result] immediately)
|
|
1445
|
+
* - **RETURNS** error tuple (same pattern, synchronous)
|
|
1446
|
+
* - Use when: Synchronous operations, explicit error handling
|
|
1447
|
+
* - Pattern: `const [error, result] = tryCatchSync(...)`
|
|
1448
|
+
*
|
|
1449
|
+
* - **`wrap(operation, options)`** ← Different error pattern
|
|
1450
|
+
* - **ASYNC ONLY**
|
|
1451
|
+
* - **THROWS** BaseException on error (traditional exceptions)
|
|
1452
|
+
* - Use when: You want errors to bubble up to handlers
|
|
1453
|
+
* - Pattern: `try { await wrap(...) } catch (e) { ... }`
|
|
1454
|
+
*
|
|
1455
|
+
* **Type-safe**: When called on subclasses, returns that subclass type in the error position.
|
|
1456
|
+
*
|
|
1457
|
+
* @template TResult - The return type of the async operation
|
|
1458
|
+
* @template TDetails - Type of exception details
|
|
1459
|
+
* @template TCause - Type of exception cause
|
|
1460
|
+
* @param this - The exception constructor (auto-bound)
|
|
1461
|
+
* @param operation - Async function to execute
|
|
1462
|
+
* @param options - Optional exception metadata if an error occurs
|
|
1463
|
+
* @returns Promise resolving to tuple of `[error, null]` or `[null, result]` (never throws)
|
|
1464
|
+
*
|
|
1465
|
+
* @example
|
|
1466
|
+
* ```typescript
|
|
1467
|
+
* // Basic async operation
|
|
1468
|
+
* const [error, data] = await BaseException.tryCatch(async () => {
|
|
1469
|
+
* const response = await fetch('/api/users');
|
|
1470
|
+
* return response.json();
|
|
1471
|
+
* });
|
|
1472
|
+
*
|
|
1473
|
+
* if (error) {
|
|
1474
|
+
* console.error('API call failed:', error.message);
|
|
1475
|
+
* return;
|
|
1476
|
+
* }
|
|
1477
|
+
*
|
|
1478
|
+
* console.log('Users:', data);
|
|
1479
|
+
* ```
|
|
1480
|
+
*
|
|
1481
|
+
* @example
|
|
1482
|
+
* ```typescript
|
|
1483
|
+
* // With error metadata
|
|
1484
|
+
* const [error, user] = await BaseException.tryCatch(
|
|
1485
|
+
* async () => db.users.findById(userId),
|
|
1486
|
+
* {
|
|
1487
|
+
* code: 'DB_ERROR',
|
|
1488
|
+
* statusCode: 500,
|
|
1489
|
+
* details: { operation: 'findUser', userId }
|
|
1490
|
+
* }
|
|
1491
|
+
* );
|
|
1492
|
+
* ```
|
|
1493
|
+
*
|
|
1494
|
+
* @example
|
|
1495
|
+
* ```typescript
|
|
1496
|
+
* // Async operation chain
|
|
1497
|
+
* async function processUserData(userId: number) {
|
|
1498
|
+
* const [userError, user] = await BaseException.tryCatch(() =>
|
|
1499
|
+
* fetchUser(userId)
|
|
1500
|
+
* );
|
|
1501
|
+
* if (userError) return { error: userError };
|
|
1502
|
+
*
|
|
1503
|
+
* const [ordersError, orders] = await BaseException.tryCatch(() =>
|
|
1504
|
+
* fetchOrders(user.id)
|
|
1505
|
+
* );
|
|
1506
|
+
* if (ordersError) return { error: ordersError };
|
|
1507
|
+
*
|
|
1508
|
+
* return { data: { user, orders } };
|
|
1509
|
+
* }
|
|
1510
|
+
* ```
|
|
1511
|
+
*
|
|
1512
|
+
* @example
|
|
1513
|
+
* ```typescript
|
|
1514
|
+
* // Parallel operations
|
|
1515
|
+
* const results = await Promise.all([
|
|
1516
|
+
* BaseException.tryCatch(() => fetchUsers()),
|
|
1517
|
+
* BaseException.tryCatch(() => fetchPosts()),
|
|
1518
|
+
* BaseException.tryCatch(() => fetchComments())
|
|
1519
|
+
* ]);
|
|
1520
|
+
*
|
|
1521
|
+
* const [usersResult, postsResult, commentsResult] = results;
|
|
1522
|
+
*
|
|
1523
|
+
* const allSucceeded = results.every(([err]) => err === null);
|
|
1524
|
+
* if (allSucceeded) {
|
|
1525
|
+
* const [, users] = usersResult;
|
|
1526
|
+
* const [, posts] = postsResult;
|
|
1527
|
+
* const [, comments] = commentsResult;
|
|
1528
|
+
* // All operations succeeded
|
|
1529
|
+
* }
|
|
1530
|
+
* ```
|
|
1531
|
+
*
|
|
1532
|
+
* @example
|
|
1533
|
+
* ```typescript
|
|
1534
|
+
* // With custom exception class
|
|
1535
|
+
* class ApiException extends BaseException {
|
|
1536
|
+
* // Custom properties/methods
|
|
1537
|
+
* }
|
|
1538
|
+
*
|
|
1539
|
+
* const [error, response] = await ApiException.tryCatch(
|
|
1540
|
+
* async () => callExternalAPI()
|
|
1541
|
+
* );
|
|
1542
|
+
*
|
|
1543
|
+
* if (error) {
|
|
1544
|
+
* // error is typed as ApiException
|
|
1545
|
+
* handleApiError(error);
|
|
1546
|
+
* }
|
|
1547
|
+
* ```
|
|
1548
|
+
*
|
|
1549
|
+
* @example
|
|
1550
|
+
* ```typescript
|
|
1551
|
+
* // API endpoint handler
|
|
1552
|
+
* app.get('/users/:id', async (req, res) => {
|
|
1553
|
+
* const [error, user] = await BaseException.tryCatch(
|
|
1554
|
+
* async () => userService.getById(req.params.id),
|
|
1555
|
+
* { code: 'USER_FETCH_ERROR' }
|
|
1556
|
+
* );
|
|
1557
|
+
*
|
|
1558
|
+
* if (error) {
|
|
1559
|
+
* return res.status(error.statusCode || 500).json(
|
|
1560
|
+
* error.toJSON()
|
|
1561
|
+
* );
|
|
1562
|
+
* }
|
|
1563
|
+
*
|
|
1564
|
+
* res.json(user);
|
|
1565
|
+
* });
|
|
1566
|
+
* ```
|
|
1567
|
+
*
|
|
1568
|
+
* @remarks
|
|
1569
|
+
* **Async Result Pattern**: Returns `Promise<[error, null] | [null, result]>`
|
|
1570
|
+
* - Eliminates async/await try-catch blocks
|
|
1571
|
+
* - Makes async error handling explicit and type-safe
|
|
1572
|
+
* - Works perfectly with Promise.all() for parallel operations
|
|
1573
|
+
* - Great for API handlers and service methods
|
|
1574
|
+
*
|
|
1575
|
+
* @see {@link tryCatchSync} - Synchronous version
|
|
1576
|
+
* @see {@link wrap} - Throws on error instead of returning tuple
|
|
1577
|
+
* @see {@link from} - The conversion method used internally
|
|
1578
|
+
*/
|
|
1579
|
+
static tryCatch<TResult, TDetails = unknown, TCause = unknown>(this: BaseExceptionConstructor<TDetails, BaseException<TDetails>>, operation: () => Promise<TResult>, options?: BaseExceptionOptions<TDetails, TCause>): Promise<[BaseException<TDetails>, null] | [null, TResult]>;
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Configuration options for creating or customizing a BaseException.
|
|
1583
|
+
*
|
|
1584
|
+
* This interface defines the optional metadata that can be provided when creating
|
|
1585
|
+
* exceptions via the constructor, `from()`, or other factory methods. All properties
|
|
1586
|
+
* are optional, allowing flexible error creation with varying levels of detail.
|
|
1587
|
+
*
|
|
1588
|
+
* @template TDetails - Type of the structured details object.
|
|
1589
|
+
* @template TCause - Type of the structured cause
|
|
1590
|
+
*
|
|
1591
|
+
* @example
|
|
1592
|
+
* ```typescript
|
|
1593
|
+
* // Basic options
|
|
1594
|
+
* const options: BaseExceptionOptions = {
|
|
1595
|
+
* code: 'USER_NOT_FOUND',
|
|
1596
|
+
* statusCode: 404
|
|
1597
|
+
* };
|
|
1598
|
+
*
|
|
1599
|
+
* const error = new BaseException('User not found', options);
|
|
1600
|
+
* ```
|
|
1601
|
+
*
|
|
1602
|
+
* @example
|
|
1603
|
+
* ```typescript
|
|
1604
|
+
* // With typed details
|
|
1605
|
+
* interface PaymentDetails {
|
|
1606
|
+
* transactionId: string;
|
|
1607
|
+
* amount: number;
|
|
1608
|
+
* currency: string;
|
|
1609
|
+
* }
|
|
1610
|
+
*
|
|
1611
|
+
* const options: BaseExceptionOptions<PaymentDetails> = {
|
|
1612
|
+
* code: 'PAYMENT_FAILED',
|
|
1613
|
+
* statusCode: 402,
|
|
1614
|
+
* details: {
|
|
1615
|
+
* transactionId: 'tx_12345',
|
|
1616
|
+
* amount: 99.99,
|
|
1617
|
+
* currency: 'USD'
|
|
1618
|
+
* }
|
|
1619
|
+
* };
|
|
1620
|
+
*
|
|
1621
|
+
* const error = new BaseException<PaymentDetails>('Payment processing failed', options);
|
|
1622
|
+
* ```
|
|
1623
|
+
*
|
|
1624
|
+
* @example
|
|
1625
|
+
* ```typescript
|
|
1626
|
+
* // With error chain (cause)
|
|
1627
|
+
* try {
|
|
1628
|
+
* await database.connect();
|
|
1629
|
+
* } catch (dbError) {
|
|
1630
|
+
* throw new BaseException('Failed to connect to database', {
|
|
1631
|
+
* code: 'DB_CONNECTION_ERROR',
|
|
1632
|
+
* statusCode: 503,
|
|
1633
|
+
* cause: dbError, // Preserves the original error
|
|
1634
|
+
* details: {
|
|
1635
|
+
* host: 'localhost',
|
|
1636
|
+
* port: 5432
|
|
1637
|
+
* }
|
|
1638
|
+
* });
|
|
1639
|
+
* }
|
|
1640
|
+
* ```
|
|
1641
|
+
*
|
|
1642
|
+
* @example
|
|
1643
|
+
* ```typescript
|
|
1644
|
+
* // With custom timestamp (for replaying/testing)
|
|
1645
|
+
* const options: BaseExceptionOptions = {
|
|
1646
|
+
* code: 'TEST_ERROR',
|
|
1647
|
+
* timestamp: new Date('2024-01-15T10:30:00Z'), // Specific timestamp
|
|
1648
|
+
* details: { testCase: 'scenario-1' }
|
|
1649
|
+
* };
|
|
1650
|
+
* ```
|
|
1651
|
+
*
|
|
1652
|
+
* @example
|
|
1653
|
+
* ```typescript
|
|
1654
|
+
* // Using fallbackMessage for error conversion
|
|
1655
|
+
* const unknownError: unknown = someOperation();
|
|
1656
|
+
*
|
|
1657
|
+
* const error = BaseException.from(unknownError, {
|
|
1658
|
+
* code: 'OPERATION_FAILED',
|
|
1659
|
+
* fallbackMessage: 'An unknown error occurred', // Used if error has no message
|
|
1660
|
+
* statusCode: 500
|
|
1661
|
+
* });
|
|
1662
|
+
* ```
|
|
1663
|
+
*/
|
|
1664
|
+
export interface BaseExceptionOptions<TDetails = unknown, TCause = unknown> {
|
|
1665
|
+
/**
|
|
1666
|
+
* Application-specific error code for categorizing errors.
|
|
1667
|
+
*
|
|
1668
|
+
* Error codes should be unique, uppercase strings with underscores (e.g., `USER_NOT_FOUND`).
|
|
1669
|
+
* They're useful for:
|
|
1670
|
+
* - Error categorization and filtering
|
|
1671
|
+
* - Client-side error handling logic
|
|
1672
|
+
* - Logging and monitoring
|
|
1673
|
+
* - Internationalization (mapping codes to localized messages)
|
|
1674
|
+
*
|
|
1675
|
+
* @example
|
|
1676
|
+
* ```typescript
|
|
1677
|
+
* { code: 'VALIDATION_ERROR' }
|
|
1678
|
+
* { code: 'AUTH_TOKEN_EXPIRED' }
|
|
1679
|
+
* { code: 'DB_CONSTRAINT_VIOLATION' }
|
|
1680
|
+
* ```
|
|
1681
|
+
*/
|
|
1682
|
+
code?: string;
|
|
1683
|
+
/**
|
|
1684
|
+
* HTTP status code associated with this exception.
|
|
1685
|
+
*
|
|
1686
|
+
* Use standard HTTP status codes to indicate the type of error:
|
|
1687
|
+
* - `400-499`: Client errors (bad request, unauthorized, not found, etc.)
|
|
1688
|
+
* - `500-599`: Server errors (internal error, service unavailable, etc.)
|
|
1689
|
+
*
|
|
1690
|
+
* Defaults to `500` (Internal Server Error) when creating exceptions via `from()`.
|
|
1691
|
+
*
|
|
1692
|
+
* @example
|
|
1693
|
+
* ```typescript
|
|
1694
|
+
* { statusCode: 400 } // Bad Request
|
|
1695
|
+
* { statusCode: 401 } // Unauthorized
|
|
1696
|
+
* { statusCode: 404 } // Not Found
|
|
1697
|
+
* { statusCode: 409 } // Conflict
|
|
1698
|
+
* { statusCode: 500 } // Internal Server Error
|
|
1699
|
+
* { statusCode: 503 } // Service Unavailable
|
|
1700
|
+
* ```
|
|
1701
|
+
*/
|
|
1702
|
+
statusCode?: number;
|
|
1703
|
+
/**
|
|
1704
|
+
* Structured additional details about the exception.
|
|
1705
|
+
*
|
|
1706
|
+
* Use this to include any domain-specific context that helps with:
|
|
1707
|
+
* - Debugging (input values, state information)
|
|
1708
|
+
* - Client-side handling (field-specific validation errors)
|
|
1709
|
+
* - Logging and monitoring (request IDs, user context)
|
|
1710
|
+
* - Error recovery (retry information, alternative actions)
|
|
1711
|
+
*
|
|
1712
|
+
* The type is generic to ensure type safety for your specific error details.
|
|
1713
|
+
*
|
|
1714
|
+
* @example
|
|
1715
|
+
* ```typescript
|
|
1716
|
+
* // Validation details
|
|
1717
|
+
* {
|
|
1718
|
+
* details: {
|
|
1719
|
+
* field: 'email',
|
|
1720
|
+
* value: 'invalid-email',
|
|
1721
|
+
* rule: 'email_format'
|
|
1722
|
+
* }
|
|
1723
|
+
* }
|
|
1724
|
+
* ```
|
|
1725
|
+
*
|
|
1726
|
+
* @example
|
|
1727
|
+
* ```typescript
|
|
1728
|
+
* // API error details
|
|
1729
|
+
* {
|
|
1730
|
+
* details: {
|
|
1731
|
+
* endpoint: '/api/users',
|
|
1732
|
+
* method: 'POST',
|
|
1733
|
+
* requestId: 'req_abc123',
|
|
1734
|
+
* responseTime: 523
|
|
1735
|
+
* }
|
|
1736
|
+
* }
|
|
1737
|
+
* ```
|
|
1738
|
+
*
|
|
1739
|
+
* @example
|
|
1740
|
+
* ```typescript
|
|
1741
|
+
* // Database error details
|
|
1742
|
+
* {
|
|
1743
|
+
* details: {
|
|
1744
|
+
* query: 'SELECT * FROM users WHERE id = ?',
|
|
1745
|
+
* table: 'users',
|
|
1746
|
+
* constraint: 'users_email_unique'
|
|
1747
|
+
* }
|
|
1748
|
+
* }
|
|
1749
|
+
* ```
|
|
1750
|
+
*/
|
|
1751
|
+
details?: BaseExceptionDetails<TDetails>;
|
|
1752
|
+
/**
|
|
1753
|
+
* The underlying error that caused this exception.
|
|
1754
|
+
*
|
|
1755
|
+
* Preserves the error chain for debugging. This is automatically set when using
|
|
1756
|
+
* `from()` to convert errors. The cause is:
|
|
1757
|
+
* - Preserved in the exception's `cause` property
|
|
1758
|
+
* - Included in `toJSON()` output (serialized recursively)
|
|
1759
|
+
* - Available for logging and error tracking
|
|
1760
|
+
*
|
|
1761
|
+
* Follows the standard Error.cause pattern from ECMAScript.
|
|
1762
|
+
*
|
|
1763
|
+
* @example
|
|
1764
|
+
* ```typescript
|
|
1765
|
+
* try {
|
|
1766
|
+
* const data = JSON.parse(invalidJson);
|
|
1767
|
+
* } catch (parseError) {
|
|
1768
|
+
* throw new BaseException('Failed to parse configuration', {
|
|
1769
|
+
* cause: parseError // Original SyntaxError preserved
|
|
1770
|
+
* });
|
|
1771
|
+
* }
|
|
1772
|
+
* ```
|
|
1773
|
+
*
|
|
1774
|
+
* @example
|
|
1775
|
+
* ```typescript
|
|
1776
|
+
* // Error chain
|
|
1777
|
+
* const networkError = new Error('ETIMEDOUT');
|
|
1778
|
+
* const apiError = new BaseException('API request failed', {
|
|
1779
|
+
* cause: networkError
|
|
1780
|
+
* });
|
|
1781
|
+
* const appError = new BaseException('User fetch failed', {
|
|
1782
|
+
* cause: apiError
|
|
1783
|
+
* });
|
|
1784
|
+
*
|
|
1785
|
+
* // The entire chain is preserved and can be serialized
|
|
1786
|
+
* console.log(appError.toJSON().cause);
|
|
1787
|
+
* // Includes apiError with its cause networkError
|
|
1788
|
+
* ```
|
|
1789
|
+
*/
|
|
1790
|
+
cause?: TCause;
|
|
1791
|
+
/**
|
|
1792
|
+
* Custom timestamp for when the exception occurred.
|
|
1793
|
+
*
|
|
1794
|
+
* Defaults to `new Date()` when the exception is created. Override this when:
|
|
1795
|
+
* - Replaying/reconstructing exceptions from logs
|
|
1796
|
+
* - Testing with fixed timestamps
|
|
1797
|
+
* - Forwarding exceptions from other systems
|
|
1798
|
+
*
|
|
1799
|
+
* @example
|
|
1800
|
+
* ```typescript
|
|
1801
|
+
* // Reconstructing from logs
|
|
1802
|
+
* {
|
|
1803
|
+
* timestamp: new Date('2024-01-15T10:30:45.123Z')
|
|
1804
|
+
* }
|
|
1805
|
+
* ```
|
|
1806
|
+
*
|
|
1807
|
+
* @example
|
|
1808
|
+
* ```typescript
|
|
1809
|
+
* // Fixed timestamp for testing
|
|
1810
|
+
* {
|
|
1811
|
+
* timestamp: new Date('2024-01-01T00:00:00Z')
|
|
1812
|
+
* }
|
|
1813
|
+
* ```
|
|
1814
|
+
*/
|
|
1815
|
+
timestamp?: Date;
|
|
1816
|
+
/**
|
|
1817
|
+
* Fallback message to use if the error source has no message.
|
|
1818
|
+
*
|
|
1819
|
+
* Only used by `from()` and related methods when converting errors that don't
|
|
1820
|
+
* have a message property. Useful for providing context-specific default messages.
|
|
1821
|
+
*
|
|
1822
|
+
* @example
|
|
1823
|
+
* ```typescript
|
|
1824
|
+
* // Converting unknown errors
|
|
1825
|
+
* const error = BaseException.from(unknownValue, {
|
|
1826
|
+
* fallbackMessage: 'Database operation failed',
|
|
1827
|
+
* code: 'DB_ERROR'
|
|
1828
|
+
* });
|
|
1829
|
+
* // If unknownValue has no message, "Database operation failed" is used
|
|
1830
|
+
* ```
|
|
1831
|
+
*
|
|
1832
|
+
* @example
|
|
1833
|
+
* ```typescript
|
|
1834
|
+
* // Context-specific fallbacks
|
|
1835
|
+
* function wrapApiCall(apiError: unknown) {
|
|
1836
|
+
* return BaseException.from(apiError, {
|
|
1837
|
+
* fallbackMessage: 'External API request failed',
|
|
1838
|
+
* code: 'API_ERROR'
|
|
1839
|
+
* });
|
|
1840
|
+
* }
|
|
1841
|
+
* ```
|
|
1842
|
+
*/
|
|
1843
|
+
fallbackMessage?: string;
|
|
1844
|
+
}
|
|
1845
|
+
/**
|
|
1846
|
+
* Type representing a constructor (class) for BaseException or its subclasses.
|
|
1847
|
+
*
|
|
1848
|
+
* This is a **constructor type**, not an instance type. It represents the type of the
|
|
1849
|
+
* class itself (the constructor function) rather than instances created by the class.
|
|
1850
|
+
*
|
|
1851
|
+
* This type is essential for proper TypeScript typing of static methods that need to
|
|
1852
|
+
* reference `this` in a way that works correctly with inheritance. When a static method
|
|
1853
|
+
* uses `this: BaseExceptionConstructor<TDetails, TException>`, TypeScript understands
|
|
1854
|
+
* that `this` refers to the constructor being called, enabling proper type inference
|
|
1855
|
+
* for subclasses.
|
|
1856
|
+
*
|
|
1857
|
+
* @template TDetails - The type of the exception details object.
|
|
1858
|
+
* @template TException - The type of exception instance the constructor creates. Must extend BaseException<TDetails>.
|
|
1859
|
+
*
|
|
1860
|
+
* @example
|
|
1861
|
+
* ```typescript
|
|
1862
|
+
* // Basic usage in a static method
|
|
1863
|
+
* class MyException extends BaseException {
|
|
1864
|
+
* static customMethod<
|
|
1865
|
+
* TDetails = unknown,
|
|
1866
|
+
* T extends BaseException<TDetails> = BaseException<TDetails>
|
|
1867
|
+
* >(
|
|
1868
|
+
* this: BaseExceptionConstructor<TDetails, T>,
|
|
1869
|
+
* message: string
|
|
1870
|
+
* ): T {
|
|
1871
|
+
* return new this(message); // `this` correctly refers to the constructor
|
|
1872
|
+
* }
|
|
1873
|
+
* }
|
|
1874
|
+
* ```
|
|
1875
|
+
*
|
|
1876
|
+
* @example
|
|
1877
|
+
* ```typescript
|
|
1878
|
+
* // Why this is needed: Without proper typing, subclass static methods lose type information
|
|
1879
|
+
* interface ApiErrorDetails {
|
|
1880
|
+
* endpoint: string;
|
|
1881
|
+
* }
|
|
1882
|
+
*
|
|
1883
|
+
* class ApiException extends BaseException<ApiErrorDetails> {
|
|
1884
|
+
* // Using BaseExceptionConstructor ensures proper typing
|
|
1885
|
+
* }
|
|
1886
|
+
*
|
|
1887
|
+
* // When calling ApiException.from(), TypeScript knows the return type is ApiException
|
|
1888
|
+
* const ex = ApiException.from(new Error('API failed'));
|
|
1889
|
+
* // ex is typed as ApiException, not BaseException
|
|
1890
|
+
* ```
|
|
1891
|
+
*
|
|
1892
|
+
*
|
|
1893
|
+
* @remarks
|
|
1894
|
+
* This follows TypeScript's convention for constructor types, similar to built-in types
|
|
1895
|
+
* like `ErrorConstructor`, `PromiseConstructor`, etc.
|
|
1896
|
+
*
|
|
1897
|
+
* @see {@link BaseException.from} - Uses this type for proper subclass typing
|
|
1898
|
+
* @see {@link BaseException.create} - Uses this type for proper subclass typing
|
|
1899
|
+
* @see {@link BaseException.createFromError} - Uses this type for proper subclass typing
|
|
1900
|
+
*/
|
|
1901
|
+
export type BaseExceptionConstructor<TDetails = unknown, TException extends BaseException<TDetails> = BaseException<TDetails>, TCause = unknown> = new (message: string, options?: BaseExceptionOptions<TDetails, TCause>) => TException;
|
|
1902
|
+
/**
|
|
1903
|
+
* Helper type for exception details.
|
|
1904
|
+
* Defaults to a loose dictionary if no specific type is provided.
|
|
1905
|
+
*/
|
|
1906
|
+
export type BaseExceptionDetails<TDetails = unknown> = unknown extends TDetails ? Record<string, any> : {
|
|
1907
|
+
[K in keyof TDetails as TDetails[K] extends (...args: unknown[]) => unknown ? never : K]: TDetails[K];
|
|
1908
|
+
};
|