@pencroff-lab/kore 0.1.0
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/LICENSE +201 -0
- package/dist/cjs/index.d.ts +3 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +19 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/src/types/err.d.ts +1116 -0
- package/dist/cjs/src/types/err.d.ts.map +1 -0
- package/dist/cjs/src/types/err.js +1324 -0
- package/dist/cjs/src/types/err.js.map +1 -0
- package/dist/cjs/src/types/index.d.ts +3 -0
- package/dist/cjs/src/types/index.d.ts.map +1 -0
- package/dist/cjs/src/types/index.js +19 -0
- package/dist/cjs/src/types/index.js.map +1 -0
- package/dist/cjs/src/types/outcome.d.ts +1002 -0
- package/dist/cjs/src/types/outcome.d.ts.map +1 -0
- package/dist/cjs/src/types/outcome.js +958 -0
- package/dist/cjs/src/types/outcome.js.map +1 -0
- package/dist/cjs/src/utils/format_dt.d.ts +9 -0
- package/dist/cjs/src/utils/format_dt.d.ts.map +1 -0
- package/dist/cjs/src/utils/format_dt.js +29 -0
- package/dist/cjs/src/utils/format_dt.js.map +1 -0
- package/dist/cjs/src/utils/index.d.ts +2 -0
- package/dist/cjs/src/utils/index.d.ts.map +1 -0
- package/dist/cjs/src/utils/index.js +18 -0
- package/dist/cjs/src/utils/index.js.map +1 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/src/types/err.d.ts +1116 -0
- package/dist/esm/src/types/err.d.ts.map +1 -0
- package/dist/esm/src/types/err.js +1320 -0
- package/dist/esm/src/types/err.js.map +1 -0
- package/dist/esm/src/types/index.d.ts +3 -0
- package/dist/esm/src/types/index.d.ts.map +1 -0
- package/dist/esm/src/types/index.js +3 -0
- package/dist/esm/src/types/index.js.map +1 -0
- package/dist/esm/src/types/outcome.d.ts +1002 -0
- package/dist/esm/src/types/outcome.d.ts.map +1 -0
- package/dist/esm/src/types/outcome.js +954 -0
- package/dist/esm/src/types/outcome.js.map +1 -0
- package/dist/esm/src/utils/format_dt.d.ts +9 -0
- package/dist/esm/src/utils/format_dt.d.ts.map +1 -0
- package/dist/esm/src/utils/format_dt.js +26 -0
- package/dist/esm/src/utils/format_dt.js.map +1 -0
- package/dist/esm/src/utils/index.d.ts +2 -0
- package/dist/esm/src/utils/index.d.ts.map +1 -0
- package/dist/esm/src/utils/index.js +2 -0
- package/dist/esm/src/utils/index.js.map +1 -0
- package/package.json +56 -0
|
@@ -0,0 +1,1324 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Error-as-value implementation for TypeScript applications.
|
|
4
|
+
*
|
|
5
|
+
* This module provides a Go-style error handling approach where errors are
|
|
6
|
+
* passed as values rather than thrown as exceptions. The `Err` class supports
|
|
7
|
+
* both single error wrapping with context and error aggregation.
|
|
8
|
+
*
|
|
9
|
+
* ## Immutability Contract
|
|
10
|
+
*
|
|
11
|
+
* All `Err` instances are immutable. Methods that appear to modify an error
|
|
12
|
+
* (like `wrap`, `withCode`, `withMetadata`, `add`) return new instances.
|
|
13
|
+
* The original error is never mutated. This means:
|
|
14
|
+
*
|
|
15
|
+
* - Safe to pass errors across boundaries without defensive copying
|
|
16
|
+
* - Method chaining always produces new instances
|
|
17
|
+
* - No "spooky action at a distance" bugs
|
|
18
|
+
*
|
|
19
|
+
* @example Basic usage with tuple pattern
|
|
20
|
+
* ```typescript
|
|
21
|
+
* import { Err } from './err';
|
|
22
|
+
*
|
|
23
|
+
* function divide(a: number, b: number): [number, null] | [null, Err] {
|
|
24
|
+
* if (b === 0) {
|
|
25
|
+
* return [null, Err.from('Division by zero', 'MATH_ERROR')];
|
|
26
|
+
* }
|
|
27
|
+
* return [a / b, null];
|
|
28
|
+
* }
|
|
29
|
+
*
|
|
30
|
+
* const [result, err] = divide(10, 0);
|
|
31
|
+
* if (err) {
|
|
32
|
+
* console.error(err.toString());
|
|
33
|
+
* return;
|
|
34
|
+
* }
|
|
35
|
+
* console.log(result); // result is number here
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* @example Error wrapping with context
|
|
39
|
+
* ```typescript
|
|
40
|
+
* function readConfig(path: string): [Config, null] | [null, Err] {
|
|
41
|
+
* const [content, readErr] = readFile(path);
|
|
42
|
+
* if (readErr) {
|
|
43
|
+
* return [null, readErr.wrap(`Failed to read config from ${path}`)];
|
|
44
|
+
* }
|
|
45
|
+
*
|
|
46
|
+
* const [parsed, parseErr] = parseJSON(content);
|
|
47
|
+
* if (parseErr) {
|
|
48
|
+
* return [null, parseErr
|
|
49
|
+
* .wrap('Invalid config format')
|
|
50
|
+
* .withCode('CONFIG_ERROR')
|
|
51
|
+
* .withMetadata({ path })];
|
|
52
|
+
* }
|
|
53
|
+
*
|
|
54
|
+
* return [parsed as Config, null];
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* @example Static wrap for catching native errors
|
|
59
|
+
* ```typescript
|
|
60
|
+
* function parseData(raw: string): [Data, null] | [null, Err] {
|
|
61
|
+
* try {
|
|
62
|
+
* return [JSON.parse(raw), null];
|
|
63
|
+
* } catch (e) {
|
|
64
|
+
* return [null, Err.wrap('Failed to parse data', e as Error)];
|
|
65
|
+
* }
|
|
66
|
+
* }
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* @example Aggregating multiple errors
|
|
70
|
+
* ```typescript
|
|
71
|
+
* function validateUser(input: UserInput): [User, null] | [null, Err] {
|
|
72
|
+
* let errors = Err.aggregate('Validation failed');
|
|
73
|
+
*
|
|
74
|
+
* if (!input.name?.trim()) {
|
|
75
|
+
* errors = errors.add('Name is required');
|
|
76
|
+
* }
|
|
77
|
+
* if (!input.email?.includes('@')) {
|
|
78
|
+
* errors = errors.add(Err.from('Invalid email', 'INVALID_EMAIL'));
|
|
79
|
+
* }
|
|
80
|
+
* if (input.age !== undefined && input.age < 0) {
|
|
81
|
+
* errors = errors.add('Age cannot be negative');
|
|
82
|
+
* }
|
|
83
|
+
*
|
|
84
|
+
* if (errors.count > 0) {
|
|
85
|
+
* return [null, errors.withCode('VALIDATION_ERROR')];
|
|
86
|
+
* }
|
|
87
|
+
*
|
|
88
|
+
* return [input as User, null];
|
|
89
|
+
* }
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* @example Serialization for service-to-service communication
|
|
93
|
+
* ```typescript
|
|
94
|
+
* // Backend: serialize error for API response
|
|
95
|
+
* const err = Err.from('User not found', 'NOT_FOUND');
|
|
96
|
+
* res.status(404).json({ error: err.toJSON() });
|
|
97
|
+
*
|
|
98
|
+
* // Frontend: deserialize error from API response
|
|
99
|
+
* const response = await fetch('/api/user/123');
|
|
100
|
+
* if (!response.ok) {
|
|
101
|
+
* const { error } = await response.json();
|
|
102
|
+
* const err = Err.fromJSON(error);
|
|
103
|
+
* console.log(err.code); // 'NOT_FOUND'
|
|
104
|
+
* }
|
|
105
|
+
*
|
|
106
|
+
* // Public API: omit stack traces
|
|
107
|
+
* res.json({ error: err.toJSON({ stack: false }) });
|
|
108
|
+
* ```
|
|
109
|
+
*
|
|
110
|
+
* @module err
|
|
111
|
+
*/
|
|
112
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
113
|
+
exports.Err = void 0;
|
|
114
|
+
/**
|
|
115
|
+
* A value-based error type that supports wrapping and aggregation.
|
|
116
|
+
*
|
|
117
|
+
* `Err` is designed to be returned from functions instead of throwing exceptions,
|
|
118
|
+
* following the Go-style error handling pattern. It supports:
|
|
119
|
+
*
|
|
120
|
+
* - **Single errors**: Created via `Err.from()` with optional code and metadata
|
|
121
|
+
* - **Error wrapping**: Adding context to errors as they propagate up the call stack
|
|
122
|
+
* - **Error aggregation**: Collecting multiple errors under a single parent (e.g., validation)
|
|
123
|
+
* - **Serialization**: Convert to/from JSON for service-to-service communication
|
|
124
|
+
*
|
|
125
|
+
* All instances are immutable - methods return new instances rather than mutating.
|
|
126
|
+
*
|
|
127
|
+
* @example Creating errors
|
|
128
|
+
* ```typescript
|
|
129
|
+
* // From string with code (most common)
|
|
130
|
+
* const err1 = Err.from('User not found', 'NOT_FOUND');
|
|
131
|
+
*
|
|
132
|
+
* // From string with full options
|
|
133
|
+
* const err2 = Err.from('Connection timeout', {
|
|
134
|
+
* code: 'TIMEOUT',
|
|
135
|
+
* metadata: { host: 'api.example.com' }
|
|
136
|
+
* });
|
|
137
|
+
*
|
|
138
|
+
* // From native Error (preserves original stack and cause chain)
|
|
139
|
+
* try {
|
|
140
|
+
* riskyOperation();
|
|
141
|
+
* } catch (e) {
|
|
142
|
+
* const err = Err.from(e).withCode('OPERATION_FAILED');
|
|
143
|
+
* return [null, err];
|
|
144
|
+
* }
|
|
145
|
+
*
|
|
146
|
+
* // From unknown (safe for catch blocks)
|
|
147
|
+
* const err3 = Err.from(unknownValue);
|
|
148
|
+
* ```
|
|
149
|
+
*
|
|
150
|
+
* @example Wrapping errors with static method
|
|
151
|
+
* ```typescript
|
|
152
|
+
* try {
|
|
153
|
+
* await db.query(sql);
|
|
154
|
+
* } catch (e) {
|
|
155
|
+
* return [null, Err.wrap('Database query failed', e as Error)];
|
|
156
|
+
* }
|
|
157
|
+
* ```
|
|
158
|
+
*
|
|
159
|
+
* @example Aggregating errors
|
|
160
|
+
* ```typescript
|
|
161
|
+
* let errors = Err.aggregate('Multiple operations failed')
|
|
162
|
+
* .add(Err.from('Database write failed'))
|
|
163
|
+
* .add(Err.from('Cache invalidation failed'))
|
|
164
|
+
* .add('Notification send failed'); // strings are auto-wrapped
|
|
165
|
+
*
|
|
166
|
+
* console.log(errors.count); // 3
|
|
167
|
+
* console.log(errors.flatten()); // Array of all individual errors
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
class Err {
|
|
171
|
+
/**
|
|
172
|
+
* Discriminator property for type narrowing.
|
|
173
|
+
* Always "Err" for Err instances.
|
|
174
|
+
*/
|
|
175
|
+
kind = "Err";
|
|
176
|
+
/**
|
|
177
|
+
* Discriminator property for type narrowing.
|
|
178
|
+
* Always `true` for Err instances.
|
|
179
|
+
*
|
|
180
|
+
* Useful when checking values from external sources (API responses,
|
|
181
|
+
* message queues) where `instanceof` may not work.
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```typescript
|
|
185
|
+
* // Checking unknown values from API
|
|
186
|
+
* const data = await response.json();
|
|
187
|
+
* if (data.error?.isErr) {
|
|
188
|
+
* // Likely an Err-like object
|
|
189
|
+
* }
|
|
190
|
+
*
|
|
191
|
+
* // For type narrowing, prefer Err.isErr()
|
|
192
|
+
* if (Err.isErr(value)) {
|
|
193
|
+
* console.error(value.message);
|
|
194
|
+
* }
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
isErr = true;
|
|
198
|
+
/** Human-readable error message */
|
|
199
|
+
message;
|
|
200
|
+
/** Error code for programmatic handling */
|
|
201
|
+
code;
|
|
202
|
+
/** Additional contextual data */
|
|
203
|
+
metadata;
|
|
204
|
+
/**
|
|
205
|
+
* Timestamp when the error was created (ISO 8601 string).
|
|
206
|
+
*
|
|
207
|
+
* Stored as string for easy serialization and comparison.
|
|
208
|
+
*/
|
|
209
|
+
timestamp;
|
|
210
|
+
/** The wrapped/caused error (for error chains) */
|
|
211
|
+
_cause;
|
|
212
|
+
/** List of aggregated errors */
|
|
213
|
+
_errors;
|
|
214
|
+
/**
|
|
215
|
+
* Stack trace - either from original Error or captured at creation.
|
|
216
|
+
*
|
|
217
|
+
* When wrapping a native Error, this preserves the original stack
|
|
218
|
+
* for better debugging (points to actual error location).
|
|
219
|
+
*/
|
|
220
|
+
_stack;
|
|
221
|
+
/**
|
|
222
|
+
* Private constructor - use static factory methods instead.
|
|
223
|
+
* @internal
|
|
224
|
+
*/
|
|
225
|
+
constructor(message, options = {}) {
|
|
226
|
+
this.message = message;
|
|
227
|
+
this.code = options.code;
|
|
228
|
+
this.metadata = options.metadata;
|
|
229
|
+
this.timestamp = options.timestamp ?? new Date().toISOString();
|
|
230
|
+
this._cause = options.cause;
|
|
231
|
+
this._errors = options.errors ?? [];
|
|
232
|
+
// Use provided stack (e.g., from native Error) or capture new one
|
|
233
|
+
// When capturing new stack, filter out internal Err class frames
|
|
234
|
+
if (options.stack) {
|
|
235
|
+
this._stack = options.stack;
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
const rawStack = new Error().stack;
|
|
239
|
+
this._stack = rawStack ? Err._filterInternalFrames(rawStack) : undefined;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
static from(input, optionsOrCode) {
|
|
243
|
+
// Normalize options
|
|
244
|
+
const options = typeof optionsOrCode === "string"
|
|
245
|
+
? { code: optionsOrCode }
|
|
246
|
+
: (optionsOrCode ?? {});
|
|
247
|
+
// Already an Err - clone with optional overrides
|
|
248
|
+
if (Err.isErr(input)) {
|
|
249
|
+
return new Err(options.message ?? input.message, {
|
|
250
|
+
code: options.code ?? input.code,
|
|
251
|
+
cause: input._cause,
|
|
252
|
+
errors: [...input._errors],
|
|
253
|
+
metadata: { ...input.metadata, ...options.metadata },
|
|
254
|
+
stack: input._stack,
|
|
255
|
+
timestamp: input.timestamp,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
// Native Error - preserve original stack and cause chain
|
|
259
|
+
if (input instanceof Error) {
|
|
260
|
+
// Convert error.cause to Err if it's an Error or string
|
|
261
|
+
let cause;
|
|
262
|
+
if (input.cause instanceof Error) {
|
|
263
|
+
cause = Err.from(input.cause);
|
|
264
|
+
}
|
|
265
|
+
else if (typeof input.cause === "string") {
|
|
266
|
+
cause = Err.from(input.cause);
|
|
267
|
+
}
|
|
268
|
+
return new Err(options.message ?? input.message, {
|
|
269
|
+
code: options.code,
|
|
270
|
+
cause,
|
|
271
|
+
metadata: {
|
|
272
|
+
originalName: input.name,
|
|
273
|
+
...options.metadata,
|
|
274
|
+
},
|
|
275
|
+
stack: input.stack, // Use original stack for better debugging
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
// String message
|
|
279
|
+
if (typeof input === "string") {
|
|
280
|
+
return new Err(input, {
|
|
281
|
+
code: options.code,
|
|
282
|
+
metadata: options.metadata,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
// Unknown value - create generic error with original value in metadata
|
|
286
|
+
return new Err(options.message ?? "Unknown error", {
|
|
287
|
+
code: options.code ?? "UNKNOWN",
|
|
288
|
+
metadata: { originalValue: input, ...options.metadata },
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Static convenience method to wrap an error with a context message.
|
|
293
|
+
*
|
|
294
|
+
* Creates a new Err with the provided message, having the original
|
|
295
|
+
* error as its cause. This is the recommended pattern for catch blocks.
|
|
296
|
+
*
|
|
297
|
+
* @param message - Context message explaining what operation failed
|
|
298
|
+
* @param error - The original error (Err, Error, or string)
|
|
299
|
+
* @param options - Optional code and metadata for the wrapper
|
|
300
|
+
* @returns New Err instance with the original as cause
|
|
301
|
+
*
|
|
302
|
+
* @see {@link Err.prototype.wrap} for the instance method
|
|
303
|
+
*
|
|
304
|
+
* @example Basic usage in catch block
|
|
305
|
+
* ```typescript
|
|
306
|
+
* try {
|
|
307
|
+
* await db.query(sql);
|
|
308
|
+
* } catch (e) {
|
|
309
|
+
* return Err.wrap('Database query failed', e as Error);
|
|
310
|
+
* }
|
|
311
|
+
* ```
|
|
312
|
+
*
|
|
313
|
+
* @example With code and metadata
|
|
314
|
+
* ```typescript
|
|
315
|
+
* try {
|
|
316
|
+
* const user = await fetchUser(id);
|
|
317
|
+
* } catch (e) {
|
|
318
|
+
* return Err.wrap('Failed to fetch user', e as Error, {
|
|
319
|
+
* code: 'USER_FETCH_ERROR',
|
|
320
|
+
* metadata: { userId: id }
|
|
321
|
+
* });
|
|
322
|
+
* }
|
|
323
|
+
* ```
|
|
324
|
+
*/
|
|
325
|
+
static wrap(message, error, options) {
|
|
326
|
+
const cause = Err.isErr(error) ? error : Err.from(error);
|
|
327
|
+
return new Err(message, {
|
|
328
|
+
code: options?.code,
|
|
329
|
+
cause,
|
|
330
|
+
metadata: options?.metadata,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Create an aggregate error for collecting multiple errors.
|
|
335
|
+
*
|
|
336
|
+
* Useful for validation, batch operations, or any scenario where
|
|
337
|
+
* multiple errors should be collected and reported together.
|
|
338
|
+
*
|
|
339
|
+
* @param message - Parent error message describing the aggregate
|
|
340
|
+
* @param errors - Optional initial list of errors
|
|
341
|
+
* @param options - Optional code and metadata for the aggregate
|
|
342
|
+
* @returns New aggregate Err instance
|
|
343
|
+
*
|
|
344
|
+
* @example Validation
|
|
345
|
+
* ```typescript
|
|
346
|
+
* function validate(data: Input): [Valid, null] | [null, Err] {
|
|
347
|
+
* let errors = Err.aggregate('Validation failed');
|
|
348
|
+
*
|
|
349
|
+
* if (!data.email) errors = errors.add('Email is required');
|
|
350
|
+
* if (!data.name) errors = errors.add('Name is required');
|
|
351
|
+
*
|
|
352
|
+
* if (errors.count > 0) {
|
|
353
|
+
* return [null, errors.withCode('VALIDATION_ERROR')];
|
|
354
|
+
* }
|
|
355
|
+
* return [data as Valid, null];
|
|
356
|
+
* }
|
|
357
|
+
* ```
|
|
358
|
+
*
|
|
359
|
+
* @example Batch operations
|
|
360
|
+
* ```typescript
|
|
361
|
+
* async function processAll(items: Item[]): [null, Err] | [void, null] {
|
|
362
|
+
* let errors = Err.aggregate('Batch processing failed');
|
|
363
|
+
*
|
|
364
|
+
* for (const item of items) {
|
|
365
|
+
* const [, err] = await processItem(item);
|
|
366
|
+
* if (err) {
|
|
367
|
+
* errors = errors.add(err.withMetadata({ itemId: item.id }));
|
|
368
|
+
* }
|
|
369
|
+
* }
|
|
370
|
+
*
|
|
371
|
+
* if (errors.count > 0) return [null, errors];
|
|
372
|
+
* return [undefined, null];
|
|
373
|
+
* }
|
|
374
|
+
* ```
|
|
375
|
+
*/
|
|
376
|
+
static aggregate(message, errors = [], options) {
|
|
377
|
+
const wrapped = errors.map((e) => (Err.isErr(e) ? e : Err.from(e)));
|
|
378
|
+
return new Err(message, {
|
|
379
|
+
code: options?.code ?? "AGGREGATE",
|
|
380
|
+
errors: wrapped,
|
|
381
|
+
metadata: options?.metadata,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Deserialize an Err from JSON representation.
|
|
386
|
+
*
|
|
387
|
+
* Reconstructs an Err instance from its JSON form, including
|
|
388
|
+
* cause chains and aggregated errors. Validates the input structure.
|
|
389
|
+
*
|
|
390
|
+
* @param json - JSON object matching ErrJSON structure
|
|
391
|
+
* @returns Reconstructed Err instance
|
|
392
|
+
* @throws Error if json is invalid or missing required fields
|
|
393
|
+
*
|
|
394
|
+
* @see {@link toJSON} for serializing an Err to JSON
|
|
395
|
+
*
|
|
396
|
+
* @example API response handling
|
|
397
|
+
* ```typescript
|
|
398
|
+
* const response = await fetch('/api/users/123');
|
|
399
|
+
* if (!response.ok) {
|
|
400
|
+
* const body = await response.json();
|
|
401
|
+
* if (body.error) {
|
|
402
|
+
* const err = Err.fromJSON(body.error);
|
|
403
|
+
* if (err.hasCode('NOT_FOUND')) {
|
|
404
|
+
* return showNotFound();
|
|
405
|
+
* }
|
|
406
|
+
* return showError(err);
|
|
407
|
+
* }
|
|
408
|
+
* }
|
|
409
|
+
* ```
|
|
410
|
+
*
|
|
411
|
+
* @example Message queue processing
|
|
412
|
+
* ```typescript
|
|
413
|
+
* queue.on('error', (message) => {
|
|
414
|
+
* const err = Err.fromJSON(message.payload);
|
|
415
|
+
* logger.error('Task failed', { error: err.toJSON() });
|
|
416
|
+
* });
|
|
417
|
+
* ```
|
|
418
|
+
*/
|
|
419
|
+
static fromJSON(json) {
|
|
420
|
+
// Validate input is an object
|
|
421
|
+
if (!json || typeof json !== "object") {
|
|
422
|
+
throw new Error("Invalid ErrJSON: expected object");
|
|
423
|
+
}
|
|
424
|
+
const obj = json;
|
|
425
|
+
// Validate required message field
|
|
426
|
+
if (typeof obj.message !== "string") {
|
|
427
|
+
throw new Error("Invalid ErrJSON: message must be a string");
|
|
428
|
+
}
|
|
429
|
+
// Validate optional fields
|
|
430
|
+
if (obj.code !== undefined && typeof obj.code !== "string") {
|
|
431
|
+
throw new Error("Invalid ErrJSON: code must be a string");
|
|
432
|
+
}
|
|
433
|
+
if (obj.timestamp !== undefined && typeof obj.timestamp !== "string") {
|
|
434
|
+
throw new Error("Invalid ErrJSON: timestamp must be a string");
|
|
435
|
+
}
|
|
436
|
+
if (obj.stack !== undefined && typeof obj.stack !== "string") {
|
|
437
|
+
throw new Error("Invalid ErrJSON: stack must be a string");
|
|
438
|
+
}
|
|
439
|
+
if (obj.metadata !== undefined && typeof obj.metadata !== "object") {
|
|
440
|
+
throw new Error("Invalid ErrJSON: metadata must be an object");
|
|
441
|
+
}
|
|
442
|
+
if (obj.errors !== undefined && !Array.isArray(obj.errors)) {
|
|
443
|
+
throw new Error("Invalid ErrJSON: errors must be an array");
|
|
444
|
+
}
|
|
445
|
+
if (obj.cause !== undefined &&
|
|
446
|
+
obj.cause !== null &&
|
|
447
|
+
typeof obj.cause !== "object") {
|
|
448
|
+
throw new Error("Invalid ErrJSON: cause must be an object");
|
|
449
|
+
}
|
|
450
|
+
// Recursively parse cause and errors
|
|
451
|
+
let cause;
|
|
452
|
+
if (obj.cause) {
|
|
453
|
+
cause = Err.fromJSON(obj.cause);
|
|
454
|
+
}
|
|
455
|
+
else if (obj._cause && typeof obj._cause === "object") {
|
|
456
|
+
cause = Err.fromJSON(obj._cause);
|
|
457
|
+
}
|
|
458
|
+
const errors = [];
|
|
459
|
+
if (Array.isArray(obj.errors)) {
|
|
460
|
+
for (const e of obj.errors) {
|
|
461
|
+
errors.push(Err.fromJSON(e));
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
else if (Array.isArray(obj._errors)) {
|
|
465
|
+
for (const e of obj._errors) {
|
|
466
|
+
errors.push(Err.fromJSON(e));
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return new Err(obj.message, {
|
|
470
|
+
code: obj.code,
|
|
471
|
+
metadata: obj.metadata,
|
|
472
|
+
timestamp: obj.timestamp,
|
|
473
|
+
stack: obj.stack,
|
|
474
|
+
cause,
|
|
475
|
+
errors,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Type guard to check if a value is an Err instance.
|
|
480
|
+
*
|
|
481
|
+
* Useful for checking values from external sources where
|
|
482
|
+
* `instanceof` may not work (different realms, serialization).
|
|
483
|
+
*
|
|
484
|
+
* @param value - Any value to check
|
|
485
|
+
* @returns `true` if value is an Err instance
|
|
486
|
+
*
|
|
487
|
+
* @example Checking external/unknown values
|
|
488
|
+
* ```typescript
|
|
489
|
+
* // Useful for values from external sources
|
|
490
|
+
* function handleApiResponse(data: unknown): void {
|
|
491
|
+
* if (Err.isErr(data)) {
|
|
492
|
+
* console.error('Received error:', data.message);
|
|
493
|
+
* return;
|
|
494
|
+
* }
|
|
495
|
+
* // Process data...
|
|
496
|
+
* }
|
|
497
|
+
* ```
|
|
498
|
+
*
|
|
499
|
+
* @example With tuple pattern (preferred for known types)
|
|
500
|
+
* ```typescript
|
|
501
|
+
* function getUser(id: string): [User, null] | [null, Err] {
|
|
502
|
+
* // ...
|
|
503
|
+
* }
|
|
504
|
+
*
|
|
505
|
+
* const [user, err] = getUser('123');
|
|
506
|
+
* if (err) {
|
|
507
|
+
* console.error(err.message);
|
|
508
|
+
* return;
|
|
509
|
+
* }
|
|
510
|
+
* console.log(user.name);
|
|
511
|
+
* ```
|
|
512
|
+
*/
|
|
513
|
+
static isErr(value) {
|
|
514
|
+
return (value instanceof Err ||
|
|
515
|
+
(!!value &&
|
|
516
|
+
typeof value === "object" &&
|
|
517
|
+
// biome-ignore lint/suspicious/noExplicitAny: value can be any in this check
|
|
518
|
+
(value.isErr === true || value.kind === "Err")));
|
|
519
|
+
}
|
|
520
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
521
|
+
// Wrapping & Context
|
|
522
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
523
|
+
/**
|
|
524
|
+
* Wrap this error with additional context.
|
|
525
|
+
*
|
|
526
|
+
* Creates a new error that has this error as its cause. The original error
|
|
527
|
+
* is preserved and accessible via `unwrap()` or `chain()`.
|
|
528
|
+
*
|
|
529
|
+
* ## Stack Trace Behavior
|
|
530
|
+
*
|
|
531
|
+
* The new wrapper captures a fresh stack trace pointing to where `wrap()`
|
|
532
|
+
* was called. This is intentional - it shows the propagation path. The
|
|
533
|
+
* original error's stack is preserved and accessible via:
|
|
534
|
+
* - `err.unwrap()?.stack` - immediate cause's stack
|
|
535
|
+
* - `err.root.stack` - original error's stack
|
|
536
|
+
*
|
|
537
|
+
* @param context - Either a message string or full options object
|
|
538
|
+
* @returns New Err instance with this error as cause
|
|
539
|
+
*
|
|
540
|
+
* @see {@link Err.wrap} for the static version (useful in catch blocks)
|
|
541
|
+
*
|
|
542
|
+
* @example Simple wrapping
|
|
543
|
+
* ```typescript
|
|
544
|
+
* const dbErr = queryDatabase();
|
|
545
|
+
* if (Err.isErr(dbErr)) {
|
|
546
|
+
* return dbErr.wrap('Failed to fetch user');
|
|
547
|
+
* }
|
|
548
|
+
* ```
|
|
549
|
+
*
|
|
550
|
+
* @example Wrapping with full options
|
|
551
|
+
* ```typescript
|
|
552
|
+
* return originalErr.wrap({
|
|
553
|
+
* message: 'Service unavailable',
|
|
554
|
+
* code: 'SERVICE_ERROR',
|
|
555
|
+
* metadata: { service: 'user-service', retryAfter: 30 }
|
|
556
|
+
* });
|
|
557
|
+
* ```
|
|
558
|
+
*
|
|
559
|
+
* @example Accessing original stack
|
|
560
|
+
* ```typescript
|
|
561
|
+
* const wrapped = original.wrap('Context 1').wrap('Context 2');
|
|
562
|
+
* console.log(wrapped.stack); // Points to second wrap() call
|
|
563
|
+
* console.log(wrapped.root.stack); // Points to original error location
|
|
564
|
+
* ```
|
|
565
|
+
*/
|
|
566
|
+
// biome-ignore lint/suspicious/useAdjacentOverloadSignatures: bug, notice static and non-static signatures as of 31/12/2025
|
|
567
|
+
wrap(context) {
|
|
568
|
+
const opts = typeof context === "string" ? { message: context } : context;
|
|
569
|
+
return new Err(opts.message ?? this.message, {
|
|
570
|
+
code: opts.code,
|
|
571
|
+
cause: this,
|
|
572
|
+
metadata: opts.metadata,
|
|
573
|
+
// New stack captured - intentional, shows wrap location
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Create a new Err with a different or added error code.
|
|
578
|
+
*
|
|
579
|
+
* Preserves the original stack trace and timestamp.
|
|
580
|
+
*
|
|
581
|
+
* @param code - The error code to set
|
|
582
|
+
* @returns New Err instance with the specified code
|
|
583
|
+
*
|
|
584
|
+
* @example
|
|
585
|
+
* ```typescript
|
|
586
|
+
* const err = Err.from('Record not found').withCode('NOT_FOUND');
|
|
587
|
+
*
|
|
588
|
+
* if (err.code === 'NOT_FOUND') {
|
|
589
|
+
* return res.status(404).json(err.toJSON());
|
|
590
|
+
* }
|
|
591
|
+
* ```
|
|
592
|
+
*/
|
|
593
|
+
withCode(code) {
|
|
594
|
+
return new Err(this.message, {
|
|
595
|
+
code,
|
|
596
|
+
cause: this._cause,
|
|
597
|
+
errors: [...this._errors],
|
|
598
|
+
metadata: this.metadata,
|
|
599
|
+
stack: this._stack,
|
|
600
|
+
timestamp: this.timestamp,
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Create a new Err with additional metadata.
|
|
605
|
+
*
|
|
606
|
+
* New metadata is merged with existing metadata. Preserves the original
|
|
607
|
+
* stack trace and timestamp.
|
|
608
|
+
*
|
|
609
|
+
* @param metadata - Key-value pairs to add to metadata
|
|
610
|
+
* @returns New Err instance with merged metadata
|
|
611
|
+
*
|
|
612
|
+
* @example
|
|
613
|
+
* ```typescript
|
|
614
|
+
* const err = Err.from('Request failed')
|
|
615
|
+
* .withMetadata({ url: '/api/users' })
|
|
616
|
+
* .withMetadata({ statusCode: 500, retryable: true });
|
|
617
|
+
*
|
|
618
|
+
* console.log(err.metadata);
|
|
619
|
+
* // { url: '/api/users', statusCode: 500, retryable: true }
|
|
620
|
+
* ```
|
|
621
|
+
*/
|
|
622
|
+
withMetadata(metadata) {
|
|
623
|
+
return new Err(this.message, {
|
|
624
|
+
code: this.code,
|
|
625
|
+
cause: this._cause,
|
|
626
|
+
errors: [...this._errors],
|
|
627
|
+
metadata: { ...this.metadata, ...metadata },
|
|
628
|
+
stack: this._stack,
|
|
629
|
+
timestamp: this.timestamp,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
633
|
+
// Aggregate Operations
|
|
634
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
635
|
+
/**
|
|
636
|
+
* Add an error to this aggregate.
|
|
637
|
+
*
|
|
638
|
+
* Returns a new Err with the error added to the list (immutable).
|
|
639
|
+
* If this is not an aggregate error, it will be treated as one with
|
|
640
|
+
* the added error as the first child.
|
|
641
|
+
*
|
|
642
|
+
* @param error - Error to add (Err, Error, or string)
|
|
643
|
+
* @returns New Err instance with the error added
|
|
644
|
+
*
|
|
645
|
+
* @example
|
|
646
|
+
* ```typescript
|
|
647
|
+
* let errors = Err.aggregate('Form validation failed');
|
|
648
|
+
*
|
|
649
|
+
* if (!email) {
|
|
650
|
+
* errors = errors.add('Email is required');
|
|
651
|
+
* }
|
|
652
|
+
* if (!password) {
|
|
653
|
+
* errors = errors.add(Err.from('Password is required').withCode('MISSING_PASSWORD'));
|
|
654
|
+
* }
|
|
655
|
+
* ```
|
|
656
|
+
*/
|
|
657
|
+
add(error) {
|
|
658
|
+
const wrapped = Err.isErr(error) ? error : Err.from(error);
|
|
659
|
+
return new Err(this.message, {
|
|
660
|
+
code: this.code,
|
|
661
|
+
cause: this._cause,
|
|
662
|
+
errors: [...this._errors, wrapped],
|
|
663
|
+
metadata: this.metadata,
|
|
664
|
+
stack: this._stack,
|
|
665
|
+
timestamp: this.timestamp,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Add multiple errors to this aggregate at once.
|
|
670
|
+
*
|
|
671
|
+
* Returns a new Err with all errors added (immutable).
|
|
672
|
+
*
|
|
673
|
+
* @param errors - Array of errors to add
|
|
674
|
+
* @returns New Err instance with all errors added
|
|
675
|
+
*
|
|
676
|
+
* @example
|
|
677
|
+
* ```typescript
|
|
678
|
+
* const validationErrors = [
|
|
679
|
+
* 'Name too short',
|
|
680
|
+
* Err.from('Invalid email format').withCode('INVALID_EMAIL'),
|
|
681
|
+
* new Error('Age must be positive'),
|
|
682
|
+
* ];
|
|
683
|
+
*
|
|
684
|
+
* const aggregate = Err.aggregate('Validation failed').addAll(validationErrors);
|
|
685
|
+
* ```
|
|
686
|
+
*/
|
|
687
|
+
addAll(errors) {
|
|
688
|
+
return errors.reduce((acc, err) => acc.add(err), this);
|
|
689
|
+
}
|
|
690
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
691
|
+
// Inspection
|
|
692
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
693
|
+
/**
|
|
694
|
+
* Whether this error is an aggregate containing multiple errors.
|
|
695
|
+
*
|
|
696
|
+
* @example
|
|
697
|
+
* ```typescript
|
|
698
|
+
* const single = Err.from('Single error');
|
|
699
|
+
* const multi = Err.aggregate('Multiple').add('One').add('Two');
|
|
700
|
+
*
|
|
701
|
+
* console.log(single.isAggregate); // false
|
|
702
|
+
* console.log(multi.isAggregate); // true
|
|
703
|
+
* ```
|
|
704
|
+
*/
|
|
705
|
+
get isAggregate() {
|
|
706
|
+
return this._errors.length > 0;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Total count of errors (including nested aggregates).
|
|
710
|
+
*
|
|
711
|
+
* For single errors, returns 1.
|
|
712
|
+
* For aggregates, recursively counts all child errors.
|
|
713
|
+
*
|
|
714
|
+
* @example
|
|
715
|
+
* ```typescript
|
|
716
|
+
* const single = Err.from('One error');
|
|
717
|
+
* console.log(single.count); // 1
|
|
718
|
+
*
|
|
719
|
+
* const nested = Err.aggregate('Parent')
|
|
720
|
+
* .add('Error 1')
|
|
721
|
+
* .add(Err.aggregate('Child').add('Error 2').add('Error 3'));
|
|
722
|
+
*
|
|
723
|
+
* console.log(nested.count); // 3
|
|
724
|
+
* ```
|
|
725
|
+
*/
|
|
726
|
+
get count() {
|
|
727
|
+
if (this.isAggregate) {
|
|
728
|
+
return this._errors.reduce((sum, e) => sum + e.count, 0);
|
|
729
|
+
}
|
|
730
|
+
return 1;
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Direct child errors (for aggregates).
|
|
734
|
+
*
|
|
735
|
+
* Returns an empty array for non-aggregate errors.
|
|
736
|
+
*
|
|
737
|
+
* @example
|
|
738
|
+
* ```typescript
|
|
739
|
+
* const aggregate = Err.aggregate('Batch failed')
|
|
740
|
+
* .add('Task 1 failed')
|
|
741
|
+
* .add('Task 2 failed');
|
|
742
|
+
*
|
|
743
|
+
* for (const err of aggregate.errors) {
|
|
744
|
+
* console.log(err.message);
|
|
745
|
+
* }
|
|
746
|
+
* // "Task 1 failed"
|
|
747
|
+
* // "Task 2 failed"
|
|
748
|
+
* ```
|
|
749
|
+
*/
|
|
750
|
+
get errors() {
|
|
751
|
+
return this._errors;
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* The root/original error in a wrapped error chain.
|
|
755
|
+
*
|
|
756
|
+
* Follows the cause chain to find the deepest error.
|
|
757
|
+
* Returns `this` if there is no cause.
|
|
758
|
+
*
|
|
759
|
+
* @example
|
|
760
|
+
* ```typescript
|
|
761
|
+
* const root = Err.from('Original error');
|
|
762
|
+
* const wrapped = root
|
|
763
|
+
* .wrap('Added context')
|
|
764
|
+
* .wrap('More context');
|
|
765
|
+
*
|
|
766
|
+
* console.log(wrapped.message); // "More context"
|
|
767
|
+
* console.log(wrapped.root.message); // "Original error"
|
|
768
|
+
* console.log(wrapped.root.stack); // Stack pointing to original error
|
|
769
|
+
* ```
|
|
770
|
+
*/
|
|
771
|
+
get root() {
|
|
772
|
+
return this._cause?.root ?? this;
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Get the directly wrapped error (one level up).
|
|
776
|
+
*
|
|
777
|
+
* Returns `undefined` if this error has no cause.
|
|
778
|
+
*
|
|
779
|
+
* @returns The wrapped Err or undefined
|
|
780
|
+
*
|
|
781
|
+
* @example
|
|
782
|
+
* ```typescript
|
|
783
|
+
* const inner = Err.from('DB connection failed');
|
|
784
|
+
* const outer = inner.wrap('Could not save user');
|
|
785
|
+
*
|
|
786
|
+
* const unwrapped = outer.unwrap();
|
|
787
|
+
* console.log(unwrapped?.message); // "DB connection failed"
|
|
788
|
+
* console.log(inner.unwrap()); // undefined
|
|
789
|
+
* ```
|
|
790
|
+
*/
|
|
791
|
+
unwrap() {
|
|
792
|
+
return this._cause;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Get the full chain of wrapped errors from root to current.
|
|
796
|
+
*
|
|
797
|
+
* The first element is the root/original error, the last is `this`.
|
|
798
|
+
*
|
|
799
|
+
* @returns Array of Err instances in causal order
|
|
800
|
+
*
|
|
801
|
+
* @remarks
|
|
802
|
+
* Time complexity: O(n) where n is the depth of the cause chain.
|
|
803
|
+
*
|
|
804
|
+
* @example
|
|
805
|
+
* ```typescript
|
|
806
|
+
* const chain = Err.from('Network timeout')
|
|
807
|
+
* .wrap('API request failed')
|
|
808
|
+
* .wrap('Could not refresh token')
|
|
809
|
+
* .wrap('Authentication failed')
|
|
810
|
+
* .chain();
|
|
811
|
+
*
|
|
812
|
+
* console.log(chain.map(e => e.message));
|
|
813
|
+
* // [
|
|
814
|
+
* // "Network timeout",
|
|
815
|
+
* // "API request failed",
|
|
816
|
+
* // "Could not refresh token",
|
|
817
|
+
* // "Authentication failed"
|
|
818
|
+
* // ]
|
|
819
|
+
* ```
|
|
820
|
+
*/
|
|
821
|
+
chain() {
|
|
822
|
+
const result = [];
|
|
823
|
+
let current = this;
|
|
824
|
+
while (current) {
|
|
825
|
+
result.unshift(current);
|
|
826
|
+
current = current._cause;
|
|
827
|
+
}
|
|
828
|
+
return result;
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Flatten all errors into a single array.
|
|
832
|
+
*
|
|
833
|
+
* For aggregates, recursively collects all leaf errors.
|
|
834
|
+
* For single errors, returns an array containing just this error.
|
|
835
|
+
*
|
|
836
|
+
* @returns Flattened array of all individual errors
|
|
837
|
+
*
|
|
838
|
+
* @remarks
|
|
839
|
+
* Time complexity: O(n) where n is the total number of errors in all nested aggregates.
|
|
840
|
+
* Recursively traverses the error tree.
|
|
841
|
+
*
|
|
842
|
+
* @example
|
|
843
|
+
* ```typescript
|
|
844
|
+
* const nested = Err.aggregate('All errors')
|
|
845
|
+
* .add('Error A')
|
|
846
|
+
* .add(Err.aggregate('Group B')
|
|
847
|
+
* .add('Error B1')
|
|
848
|
+
* .add('Error B2'))
|
|
849
|
+
* .add('Error C');
|
|
850
|
+
*
|
|
851
|
+
* const flat = nested.flatten();
|
|
852
|
+
* console.log(flat.map(e => e.message));
|
|
853
|
+
* // ["Error A", "Error B1", "Error B2", "Error C"]
|
|
854
|
+
* ```
|
|
855
|
+
*/
|
|
856
|
+
flatten() {
|
|
857
|
+
if (!this.isAggregate) {
|
|
858
|
+
return [this];
|
|
859
|
+
}
|
|
860
|
+
return this._errors.flatMap((e) => e.flatten());
|
|
861
|
+
}
|
|
862
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
863
|
+
// Matching & Filtering
|
|
864
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
865
|
+
/**
|
|
866
|
+
* Check if this error or any error in its chain/aggregate has a specific code.
|
|
867
|
+
*
|
|
868
|
+
* Searches the cause chain and all aggregated errors.
|
|
869
|
+
*
|
|
870
|
+
* @param code - The error code to search for
|
|
871
|
+
* @returns `true` if the code is found anywhere in the error tree
|
|
872
|
+
*
|
|
873
|
+
* @example
|
|
874
|
+
* ```typescript
|
|
875
|
+
* const err = Err.from('DB error', 'DB_ERROR')
|
|
876
|
+
* .wrap('Repository failed')
|
|
877
|
+
* .wrap('Service unavailable');
|
|
878
|
+
*
|
|
879
|
+
* console.log(err.hasCode('DB_ERROR')); // true
|
|
880
|
+
* console.log(err.hasCode('NETWORK_ERROR')); // false
|
|
881
|
+
* ```
|
|
882
|
+
*/
|
|
883
|
+
hasCode(code) {
|
|
884
|
+
// if (this.code === code) return true;
|
|
885
|
+
// if (this._cause?.hasCode(code)) return true;
|
|
886
|
+
// return this._errors.some((e) => e.hasCode(code));
|
|
887
|
+
return this._searchCode((c) => c === code);
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Check if this error or any error in its chain/aggregate has a code
|
|
891
|
+
* matching the given prefix with boundary awareness.
|
|
892
|
+
*
|
|
893
|
+
* This enables hierarchical error code patterns like `AUTH:TOKEN:EXPIRED`
|
|
894
|
+
* where libraries define base codes and consumers extend with subcodes.
|
|
895
|
+
*
|
|
896
|
+
* Matches if:
|
|
897
|
+
* - Code equals prefix exactly (e.g., `"AUTH"` matches `"AUTH"`)
|
|
898
|
+
* - Code starts with prefix + boundary (e.g., `"AUTH"` matches `"AUTH:EXPIRED"`)
|
|
899
|
+
*
|
|
900
|
+
* Does NOT match partial strings (e.g., `"AUTH"` does NOT match `"AUTHORIZATION"`).
|
|
901
|
+
*
|
|
902
|
+
* @param prefix - The code prefix to search for
|
|
903
|
+
* @param boundary - Separator character/string between code segments (default: ":")
|
|
904
|
+
* @returns `true` if a matching code is found anywhere in the error tree
|
|
905
|
+
*
|
|
906
|
+
* @example Basic hierarchical codes
|
|
907
|
+
* ```typescript
|
|
908
|
+
* const err = Err.from('Token expired', { code: 'AUTH:TOKEN:EXPIRED' });
|
|
909
|
+
*
|
|
910
|
+
* err.hasCodePrefix('AUTH'); // true (matches AUTH:*)
|
|
911
|
+
* err.hasCodePrefix('AUTH:TOKEN'); // true (matches AUTH:TOKEN:*)
|
|
912
|
+
* err.hasCodePrefix('AUTHORIZATION'); // false (no boundary match)
|
|
913
|
+
* ```
|
|
914
|
+
*
|
|
915
|
+
* @example Custom boundary
|
|
916
|
+
* ```typescript
|
|
917
|
+
* const err = Err.from('Not found', { code: 'HTTP.404.NOT_FOUND' });
|
|
918
|
+
*
|
|
919
|
+
* err.hasCodePrefix('HTTP', '.'); // true
|
|
920
|
+
* err.hasCodePrefix('HTTP.404', '.'); // true
|
|
921
|
+
* err.hasCodePrefix('HTTP', ':'); // false (wrong boundary)
|
|
922
|
+
* ```
|
|
923
|
+
*
|
|
924
|
+
* @example Search in error tree
|
|
925
|
+
* ```typescript
|
|
926
|
+
* const err = Err.from('DB error', { code: 'DB:CONNECTION' })
|
|
927
|
+
* .wrap('Service failed', { code: 'SERVICE:UNAVAILABLE' });
|
|
928
|
+
*
|
|
929
|
+
* err.hasCodePrefix('DB'); // true (found in cause)
|
|
930
|
+
* err.hasCodePrefix('SERVICE'); // true (found in current)
|
|
931
|
+
* ```
|
|
932
|
+
*/
|
|
933
|
+
hasCodePrefix(prefix, boundary = ":") {
|
|
934
|
+
// // Check current error's code
|
|
935
|
+
// if (this.code !== undefined) {
|
|
936
|
+
// if (this.code === prefix) return true;
|
|
937
|
+
// if (this.code.startsWith(prefix + boundary)) return true;
|
|
938
|
+
// }
|
|
939
|
+
// // Search cause chain
|
|
940
|
+
// if (this._cause?.hasCodePrefix(prefix, boundary)) return true;
|
|
941
|
+
// // Search aggregated errors
|
|
942
|
+
// return this._errors.some((e) => e.hasCodePrefix(prefix, boundary));
|
|
943
|
+
return this._searchCode((c) => c === prefix || c.startsWith(prefix + boundary));
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Find the first error matching a predicate.
|
|
947
|
+
*
|
|
948
|
+
* Searches this error, its cause chain, and all aggregated errors.
|
|
949
|
+
*
|
|
950
|
+
* @param predicate - Function to test each error
|
|
951
|
+
* @returns The first matching Err or undefined
|
|
952
|
+
*
|
|
953
|
+
* @example
|
|
954
|
+
* ```typescript
|
|
955
|
+
* const err = Err.aggregate('Multiple failures')
|
|
956
|
+
* .add(Err.from('Not found', 'NOT_FOUND'))
|
|
957
|
+
* .add(Err.from('Timeout', 'TIMEOUT'));
|
|
958
|
+
*
|
|
959
|
+
* const timeout = err.find(e => e.code === 'TIMEOUT');
|
|
960
|
+
* console.log(timeout?.message); // "Timeout"
|
|
961
|
+
* ```
|
|
962
|
+
*/
|
|
963
|
+
find(predicate) {
|
|
964
|
+
if (predicate(this))
|
|
965
|
+
return this;
|
|
966
|
+
const inCause = this._cause?.find(predicate);
|
|
967
|
+
if (inCause)
|
|
968
|
+
return inCause;
|
|
969
|
+
for (const err of this._errors) {
|
|
970
|
+
const found = err.find(predicate);
|
|
971
|
+
if (found)
|
|
972
|
+
return found;
|
|
973
|
+
}
|
|
974
|
+
return undefined;
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Find all errors matching a predicate.
|
|
978
|
+
*
|
|
979
|
+
* Searches this error, its cause chain, and all aggregated errors.
|
|
980
|
+
*
|
|
981
|
+
* @param predicate - Function to test each error
|
|
982
|
+
* @returns Array of all matching Err instances
|
|
983
|
+
*
|
|
984
|
+
* @example
|
|
985
|
+
* ```typescript
|
|
986
|
+
* const err = Err.aggregate('Validation failed')
|
|
987
|
+
* .add(Err.from('Name required', 'REQUIRED'))
|
|
988
|
+
* .add(Err.from('Invalid email', 'INVALID'))
|
|
989
|
+
* .add(Err.from('Age required', 'REQUIRED'));
|
|
990
|
+
*
|
|
991
|
+
* const required = err.filter(e => e.code === 'REQUIRED');
|
|
992
|
+
* console.log(required.length); // 2
|
|
993
|
+
* ```
|
|
994
|
+
*/
|
|
995
|
+
filter(predicate) {
|
|
996
|
+
const results = [];
|
|
997
|
+
if (predicate(this))
|
|
998
|
+
results.push(this);
|
|
999
|
+
if (this._cause)
|
|
1000
|
+
results.push(...this._cause.filter(predicate));
|
|
1001
|
+
for (const err of this._errors) {
|
|
1002
|
+
results.push(...err.filter(predicate));
|
|
1003
|
+
}
|
|
1004
|
+
return results;
|
|
1005
|
+
}
|
|
1006
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
1007
|
+
// Conversion
|
|
1008
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
1009
|
+
/**
|
|
1010
|
+
* Convert to a JSON-serializable object.
|
|
1011
|
+
*
|
|
1012
|
+
* Useful for logging, API responses, and serialization.
|
|
1013
|
+
* Use options to control what's included (e.g., omit stack for public APIs).
|
|
1014
|
+
*
|
|
1015
|
+
* @param options - Control what fields are included
|
|
1016
|
+
* @returns Plain object representation
|
|
1017
|
+
*
|
|
1018
|
+
* @see {@link fromJSON} for deserializing an Err from JSON
|
|
1019
|
+
*
|
|
1020
|
+
* @example Full serialization (default)
|
|
1021
|
+
* ```typescript
|
|
1022
|
+
* const err = Err.from('Not found', {
|
|
1023
|
+
* code: 'NOT_FOUND',
|
|
1024
|
+
* metadata: { userId: '123' }
|
|
1025
|
+
* });
|
|
1026
|
+
*
|
|
1027
|
+
* console.log(JSON.stringify(err.toJSON(), null, 2));
|
|
1028
|
+
* // {
|
|
1029
|
+
* // "message": "Not found",
|
|
1030
|
+
* // "code": "NOT_FOUND",
|
|
1031
|
+
* // "metadata": { "userId": "123" },
|
|
1032
|
+
* // "timestamp": "2024-01-15T10:30:00.000Z",
|
|
1033
|
+
* // "stack": "Error: ...",
|
|
1034
|
+
* // "errors": []
|
|
1035
|
+
* // }
|
|
1036
|
+
* ```
|
|
1037
|
+
*
|
|
1038
|
+
* @example Public API response (no stack)
|
|
1039
|
+
* ```typescript
|
|
1040
|
+
* app.get('/user/:id', (req, res) => {
|
|
1041
|
+
* const result = getUser(req.params.id);
|
|
1042
|
+
* if (Err.isErr(result)) {
|
|
1043
|
+
* const status = result.code === 'NOT_FOUND' ? 404 : 500;
|
|
1044
|
+
* return res.status(status).json({
|
|
1045
|
+
* error: result.toJSON({ stack: false })
|
|
1046
|
+
* });
|
|
1047
|
+
* }
|
|
1048
|
+
* res.json(result);
|
|
1049
|
+
* });
|
|
1050
|
+
* ```
|
|
1051
|
+
*
|
|
1052
|
+
* @example Minimal payload
|
|
1053
|
+
* ```typescript
|
|
1054
|
+
* err.toJSON({ stack: false, metadata: false });
|
|
1055
|
+
* // Only includes: message, code, timestamp, cause, errors
|
|
1056
|
+
* ```
|
|
1057
|
+
*/
|
|
1058
|
+
toJSON(options = {}) {
|
|
1059
|
+
const { stack = true, metadata = true } = options;
|
|
1060
|
+
return {
|
|
1061
|
+
message: this.message,
|
|
1062
|
+
kind: "Err",
|
|
1063
|
+
isErr: true,
|
|
1064
|
+
code: this.code,
|
|
1065
|
+
metadata: metadata ? this.metadata : undefined,
|
|
1066
|
+
timestamp: this.timestamp,
|
|
1067
|
+
stack: stack ? this._stack : undefined,
|
|
1068
|
+
cause: this._cause?.toJSON(options),
|
|
1069
|
+
errors: this._errors.map((e) => e.toJSON(options)),
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Recursive code search helper.
|
|
1074
|
+
* @param matcher
|
|
1075
|
+
* @private
|
|
1076
|
+
*/
|
|
1077
|
+
_searchCode(matcher) {
|
|
1078
|
+
if (this.code !== undefined && matcher(this.code))
|
|
1079
|
+
return true;
|
|
1080
|
+
if (this._cause?._searchCode(matcher))
|
|
1081
|
+
return true;
|
|
1082
|
+
return this._errors.some((e) => e._searchCode(matcher));
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* Pattern to identify internal Err class frames to filter out.
|
|
1086
|
+
* Matches frames from err.ts file (handles both "at Err.from" and "at from" patterns).
|
|
1087
|
+
* @internal
|
|
1088
|
+
*/
|
|
1089
|
+
static INTERNAL_FRAME_PATTERN = /\/err\.ts:\d+:\d+\)?$/;
|
|
1090
|
+
/**
|
|
1091
|
+
* Filter out internal Err class frames from stack trace.
|
|
1092
|
+
* This makes stack traces more useful by starting at user code.
|
|
1093
|
+
*
|
|
1094
|
+
* @param stack - Raw stack trace string
|
|
1095
|
+
* @returns Stack with internal frames removed
|
|
1096
|
+
* @internal
|
|
1097
|
+
*/
|
|
1098
|
+
static _filterInternalFrames(stack) {
|
|
1099
|
+
const lines = stack.split("\n");
|
|
1100
|
+
const firstLine = lines[0]; // Error message line
|
|
1101
|
+
const frames = lines.slice(1);
|
|
1102
|
+
// Find the first frame that's NOT an internal Err frame
|
|
1103
|
+
const firstUserFrameIndex = frames.findIndex((line) => !Err.INTERNAL_FRAME_PATTERN.test(line));
|
|
1104
|
+
if (firstUserFrameIndex <= 0) {
|
|
1105
|
+
// No internal frames found or already starts at user code
|
|
1106
|
+
return stack;
|
|
1107
|
+
}
|
|
1108
|
+
// Reconstruct stack without internal frames
|
|
1109
|
+
const userFrames = frames.slice(firstUserFrameIndex);
|
|
1110
|
+
return [firstLine, ...userFrames].join("\n");
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Parse and extract stack frames from the stack trace.
|
|
1114
|
+
*
|
|
1115
|
+
* @param limit - Maximum number of frames to return (undefined = all)
|
|
1116
|
+
* @returns Array of stack frame strings
|
|
1117
|
+
* @internal
|
|
1118
|
+
*/
|
|
1119
|
+
_getStackFrames(limit) {
|
|
1120
|
+
if (!this._stack)
|
|
1121
|
+
return [];
|
|
1122
|
+
const lines = this._stack.split("\n");
|
|
1123
|
+
// Skip the first line (error message) and filter to "at ..." lines
|
|
1124
|
+
const frames = lines
|
|
1125
|
+
.slice(1)
|
|
1126
|
+
.map((line) => line.trim())
|
|
1127
|
+
.filter((line) => line.startsWith("at "));
|
|
1128
|
+
if (limit !== undefined && limit > 0) {
|
|
1129
|
+
return frames.slice(0, limit);
|
|
1130
|
+
}
|
|
1131
|
+
return frames;
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Count remaining causes in the chain from a given error.
|
|
1135
|
+
*
|
|
1136
|
+
* @param err - Starting error
|
|
1137
|
+
* @returns Number of causes remaining
|
|
1138
|
+
* @internal
|
|
1139
|
+
*/
|
|
1140
|
+
_countRemainingCauses(err) {
|
|
1141
|
+
let count = 0;
|
|
1142
|
+
let current = err;
|
|
1143
|
+
while (current) {
|
|
1144
|
+
count++;
|
|
1145
|
+
current = current._cause;
|
|
1146
|
+
}
|
|
1147
|
+
return count;
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Convert to a formatted string for logging/display.
|
|
1151
|
+
*
|
|
1152
|
+
* Includes cause chain and aggregated errors with indentation.
|
|
1153
|
+
* When called with options, can include additional details like
|
|
1154
|
+
* stack traces, timestamps, and metadata.
|
|
1155
|
+
*
|
|
1156
|
+
* @param options - Formatting options (optional)
|
|
1157
|
+
* @returns Formatted error string
|
|
1158
|
+
*
|
|
1159
|
+
* @example Basic usage (no options - backward compatible)
|
|
1160
|
+
* ```typescript
|
|
1161
|
+
* const err = Err.from('DB error')
|
|
1162
|
+
* .wrap('Repository failed')
|
|
1163
|
+
* .wrap('Service unavailable');
|
|
1164
|
+
*
|
|
1165
|
+
* console.log(err.toString());
|
|
1166
|
+
* // [ERROR] Service unavailable
|
|
1167
|
+
* // Caused by: [ERROR] Repository failed
|
|
1168
|
+
* // Caused by: [ERROR] DB error
|
|
1169
|
+
* ```
|
|
1170
|
+
*
|
|
1171
|
+
* @example With options
|
|
1172
|
+
* ```typescript
|
|
1173
|
+
* const err = Err.from('Connection failed', {
|
|
1174
|
+
* code: 'DB:CONNECTION',
|
|
1175
|
+
* metadata: { host: 'localhost', port: 5432 }
|
|
1176
|
+
* });
|
|
1177
|
+
*
|
|
1178
|
+
* console.log(err.toString({ date: true, metadata: true, stack: 3 }));
|
|
1179
|
+
* // [2024-01-15T10:30:00.000Z] [DB:CONNECTION] Connection failed
|
|
1180
|
+
* // metadata: {"host":"localhost","port":5432}
|
|
1181
|
+
* // stack:
|
|
1182
|
+
* // at Database.connect (src/db.ts:45)
|
|
1183
|
+
* // at Repository.init (src/repo.ts:23)
|
|
1184
|
+
* // at Service.start (src/service.ts:12)
|
|
1185
|
+
* ```
|
|
1186
|
+
*
|
|
1187
|
+
* @example Aggregate
|
|
1188
|
+
* ```typescript
|
|
1189
|
+
* const err = Err.aggregate('Validation failed', [], { code: 'VALIDATION' })
|
|
1190
|
+
* .add('Name required')
|
|
1191
|
+
* .add('Email invalid');
|
|
1192
|
+
*
|
|
1193
|
+
* console.log(err.toString());
|
|
1194
|
+
* // [VALIDATION] Validation failed
|
|
1195
|
+
* // Errors (2):
|
|
1196
|
+
* // - [ERROR] Name required
|
|
1197
|
+
* // - [ERROR] Email invalid
|
|
1198
|
+
* ```
|
|
1199
|
+
*
|
|
1200
|
+
* @example With maxDepth limit
|
|
1201
|
+
* ```typescript
|
|
1202
|
+
* const deep = Err.from('Root')
|
|
1203
|
+
* .wrap('Level 1')
|
|
1204
|
+
* .wrap('Level 2')
|
|
1205
|
+
* .wrap('Level 3');
|
|
1206
|
+
*
|
|
1207
|
+
* console.log(deep.toString({ maxDepth: 2 }));
|
|
1208
|
+
* // [ERROR] Level 3
|
|
1209
|
+
* // Caused by: [ERROR] Level 2
|
|
1210
|
+
* // ... (2 more causes)
|
|
1211
|
+
* ```
|
|
1212
|
+
*/
|
|
1213
|
+
toString(options) {
|
|
1214
|
+
return this._toStringInternal(options, 0);
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Internal toString implementation with depth tracking.
|
|
1218
|
+
* @internal
|
|
1219
|
+
*/
|
|
1220
|
+
_toStringInternal(options, currentDepth) {
|
|
1221
|
+
const indent = options?.indent ?? " ";
|
|
1222
|
+
// Build the main error line
|
|
1223
|
+
let result = "";
|
|
1224
|
+
// Add timestamp if requested
|
|
1225
|
+
if (options?.date) {
|
|
1226
|
+
result += `[${this.timestamp}] `;
|
|
1227
|
+
}
|
|
1228
|
+
// Add code and message
|
|
1229
|
+
result += `[${this.code ?? "ERROR"}] ${this.message}`;
|
|
1230
|
+
// Add metadata if requested
|
|
1231
|
+
if (options?.metadata && this.metadata) {
|
|
1232
|
+
result += `\n${indent}metadata: ${JSON.stringify(this.metadata)}`;
|
|
1233
|
+
}
|
|
1234
|
+
// Add stack trace if requested
|
|
1235
|
+
if (options?.stack) {
|
|
1236
|
+
const frameLimit = typeof options.stack === "number" ? options.stack : undefined;
|
|
1237
|
+
const frames = this._getStackFrames(frameLimit);
|
|
1238
|
+
if (frames.length > 0) {
|
|
1239
|
+
result += `\n${indent}stack:`;
|
|
1240
|
+
for (const frame of frames) {
|
|
1241
|
+
result += `\n${indent}${indent}${frame}`;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
// Handle cause chain with depth limiting
|
|
1246
|
+
if (this._cause) {
|
|
1247
|
+
const maxDepth = options?.maxDepth;
|
|
1248
|
+
if (maxDepth !== undefined && currentDepth >= maxDepth) {
|
|
1249
|
+
// Depth limit reached - show remaining count
|
|
1250
|
+
const remaining = this._countRemainingCauses(this._cause);
|
|
1251
|
+
result += `\n${indent}... (${remaining} more cause${remaining > 1 ? "s" : ""})`;
|
|
1252
|
+
}
|
|
1253
|
+
else {
|
|
1254
|
+
// Recurse into cause
|
|
1255
|
+
const causeStr = this._cause._toStringInternal(options, currentDepth + 1);
|
|
1256
|
+
result += `\n${indent}Caused by: ${causeStr.replace(/\n/g, `\n${indent}`)}`;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
// Handle aggregated errors
|
|
1260
|
+
if (this._errors.length > 0) {
|
|
1261
|
+
result += `\n${indent}Errors (${this._errors.length}):`;
|
|
1262
|
+
for (const err of this._errors) {
|
|
1263
|
+
// Aggregated errors start at depth 0 for their own chain
|
|
1264
|
+
const errStr = err._toStringInternal(options, 0);
|
|
1265
|
+
result += `\n${indent}${indent}- ${errStr.replace(/\n/g, `\n${indent}${indent} `)}`;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
return result;
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Convert to a native Error for interop with throw-based APIs.
|
|
1272
|
+
*
|
|
1273
|
+
* Creates an Error with:
|
|
1274
|
+
* - `message`: This error's message
|
|
1275
|
+
* - `name`: This error's code (or "Err")
|
|
1276
|
+
* - `stack`: This error's original stack trace
|
|
1277
|
+
* - `cause`: Converted cause chain (native Error)
|
|
1278
|
+
*
|
|
1279
|
+
* Note: Metadata is not included on the native Error.
|
|
1280
|
+
*
|
|
1281
|
+
* @returns Native Error instance
|
|
1282
|
+
*
|
|
1283
|
+
* @example
|
|
1284
|
+
* ```typescript
|
|
1285
|
+
* const err = Err.from('Something failed', 'MY_ERROR');
|
|
1286
|
+
*
|
|
1287
|
+
* // If you need to throw for some API
|
|
1288
|
+
* throw err.toError();
|
|
1289
|
+
*
|
|
1290
|
+
* // The thrown error will have:
|
|
1291
|
+
* // - error.message === "Something failed"
|
|
1292
|
+
* // - error.name === "MY_ERROR"
|
|
1293
|
+
* // - error.stack === (original stack trace)
|
|
1294
|
+
* // - error.cause === (if wrapped)
|
|
1295
|
+
* ```
|
|
1296
|
+
*/
|
|
1297
|
+
toError() {
|
|
1298
|
+
const err = new Error(this.message);
|
|
1299
|
+
err.name = this.code ?? "Err";
|
|
1300
|
+
// Preserve original stack trace
|
|
1301
|
+
if (this._stack) {
|
|
1302
|
+
err.stack = this._stack;
|
|
1303
|
+
}
|
|
1304
|
+
// Preserve cause chain
|
|
1305
|
+
if (this._cause) {
|
|
1306
|
+
err.cause = this._cause.toError();
|
|
1307
|
+
}
|
|
1308
|
+
return err;
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Get the captured stack trace.
|
|
1312
|
+
*
|
|
1313
|
+
* For errors created from native Errors, this is the original stack.
|
|
1314
|
+
* For errors created via `Err.from(string)`, this is the stack at creation.
|
|
1315
|
+
* For wrapped errors, use `.root.stack` to get the original location.
|
|
1316
|
+
*
|
|
1317
|
+
* @returns Stack trace string or undefined
|
|
1318
|
+
*/
|
|
1319
|
+
get stack() {
|
|
1320
|
+
return this._stack;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
exports.Err = Err;
|
|
1324
|
+
//# sourceMappingURL=err.js.map
|