@pencroff-lab/kore 0.1.1 → 0.1.2
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 +8 -0
- package/dist/cjs/src/utils/format_dt.d.ts +69 -0
- package/dist/cjs/src/utils/format_dt.d.ts.map +1 -1
- package/dist/cjs/src/utils/format_dt.js +40 -0
- package/dist/cjs/src/utils/format_dt.js.map +1 -1
- package/dist/esm/src/utils/format_dt.d.ts +69 -0
- package/dist/esm/src/utils/format_dt.d.ts.map +1 -1
- package/dist/esm/src/utils/format_dt.js +40 -0
- package/dist/esm/src/utils/format_dt.js.map +1 -1
- package/docs/err.md +897 -0
- package/docs/format_dt.md +139 -0
- package/docs/outcome.md +831 -0
- package/llms.txt +22 -0
- package/package.json +4 -2
package/docs/err.md
ADDED
|
@@ -0,0 +1,897 @@
|
|
|
1
|
+
# Err
|
|
2
|
+
|
|
3
|
+
Immutable, value-based error type for TypeScript applications.
|
|
4
|
+
|
|
5
|
+
`Err` implements Go-style error handling where errors are passed as values rather than thrown as exceptions. It supports single error wrapping with context, error aggregation, hierarchical error codes, JSON serialization/deserialization, and conversion to native `Error`.
|
|
6
|
+
|
|
7
|
+
## Immutability Contract
|
|
8
|
+
|
|
9
|
+
All `Err` instances are immutable. Methods that appear to modify an error (`wrap`, `withCode`, `withMetadata`, `add`) return **new instances**. The original error is never mutated. This means:
|
|
10
|
+
|
|
11
|
+
- Safe to pass errors across boundaries without defensive copying
|
|
12
|
+
- Method chaining always produces new instances
|
|
13
|
+
- No "spooky action at a distance" bugs
|
|
14
|
+
|
|
15
|
+
## Types
|
|
16
|
+
|
|
17
|
+
### `ErrCode`
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
type ErrCode = string;
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Error code type -- typically uppercase snake_case identifiers.
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
const codes: ErrCode[] = [
|
|
27
|
+
"NOT_FOUND",
|
|
28
|
+
"VALIDATION_ERROR",
|
|
29
|
+
"DB_CONNECTION_FAILED",
|
|
30
|
+
"AUTH_EXPIRED",
|
|
31
|
+
];
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### `ErrOptions`
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
interface ErrOptions {
|
|
38
|
+
code?: ErrCode; // Error code for programmatic handling
|
|
39
|
+
message?: string; // Human-readable error message
|
|
40
|
+
metadata?: Record<string, unknown>; // Additional contextual data
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### `ErrJSONOptions`
|
|
45
|
+
|
|
46
|
+
Options for JSON serialization.
|
|
47
|
+
|
|
48
|
+
| Property | Type | Default | Description |
|
|
49
|
+
|------------|-----------|---------|------------------------------------------------------|
|
|
50
|
+
| `stack` | `boolean` | `true` | Include stack trace. Set to `false` for public APIs. |
|
|
51
|
+
| `metadata` | `boolean` | `true` | Include metadata. Set to `false` to omit sensitive data. |
|
|
52
|
+
|
|
53
|
+
### `ToStringOptions`
|
|
54
|
+
|
|
55
|
+
Options for `toString()` output formatting.
|
|
56
|
+
|
|
57
|
+
| Property | Type | Default | Description |
|
|
58
|
+
|------------|----------------------|---------------|--------------------------------------------------------|
|
|
59
|
+
| `stack` | `boolean \| number` | `undefined` | `true` for full stack, number for top N frames. |
|
|
60
|
+
| `date` | `boolean` | `false` | Include timestamp in ISO 8601 format. |
|
|
61
|
+
| `metadata` | `boolean` | `false` | Include metadata object in output. |
|
|
62
|
+
| `maxDepth` | `number` | `undefined` | Max depth for cause chain. Exceeded shows "... (N more causes)". |
|
|
63
|
+
| `indent` | `string` | `" "` | Indentation string for nested output. |
|
|
64
|
+
|
|
65
|
+
### `ErrJSON`
|
|
66
|
+
|
|
67
|
+
JSON representation of an `Err` for serialization.
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
interface ErrJSON {
|
|
71
|
+
message: string;
|
|
72
|
+
kind?: "Err";
|
|
73
|
+
isErr?: boolean;
|
|
74
|
+
code?: ErrCode;
|
|
75
|
+
metadata?: Record<string, unknown>;
|
|
76
|
+
timestamp: string;
|
|
77
|
+
stack?: string;
|
|
78
|
+
cause?: ErrJSON;
|
|
79
|
+
errors: ErrJSON[];
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Instance Properties
|
|
84
|
+
|
|
85
|
+
### `kind`
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
readonly kind: "Err"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Discriminator property for type narrowing. Always `"Err"`.
|
|
92
|
+
|
|
93
|
+
### `isErr`
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
readonly isErr: true
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Discriminator property for type narrowing. Always `true`.
|
|
100
|
+
|
|
101
|
+
Useful when checking values from external sources (API responses, message queues) where `instanceof` may not work.
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// Checking unknown values from API
|
|
105
|
+
const data = await response.json();
|
|
106
|
+
if (data.error?.isErr) {
|
|
107
|
+
// Likely an Err-like object
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// For type narrowing, prefer Err.isErr()
|
|
111
|
+
if (Err.isErr(value)) {
|
|
112
|
+
console.error(value.message);
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### `message`
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
readonly message: string
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Human-readable error message.
|
|
123
|
+
|
|
124
|
+
### `code`
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
readonly code?: ErrCode
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Error code for programmatic handling.
|
|
131
|
+
|
|
132
|
+
### `metadata`
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
readonly metadata?: Record<string, unknown>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Additional contextual data attached to the error.
|
|
139
|
+
|
|
140
|
+
### `timestamp`
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
readonly timestamp: string
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Timestamp when the error was created (ISO 8601 string). Stored as string for easy serialization and comparison.
|
|
147
|
+
|
|
148
|
+
### `stack`
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
get stack(): string | undefined
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The captured stack trace.
|
|
155
|
+
|
|
156
|
+
- For errors created from native `Error`s, this is the original stack.
|
|
157
|
+
- For errors created via `Err.from(string)`, this is the stack at creation.
|
|
158
|
+
- For wrapped errors, use `.root.stack` to get the original location.
|
|
159
|
+
|
|
160
|
+
## Static Constructors
|
|
161
|
+
|
|
162
|
+
### `Err.from()`
|
|
163
|
+
|
|
164
|
+
Create an `Err` from various input types.
|
|
165
|
+
|
|
166
|
+
#### From string with optional code
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
static from(message: string, code?: ErrCode): Err
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
const err = Err.from("User not found", "NOT_FOUND");
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### From string with full options
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
static from(message: string, options: ErrOptions): Err
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
const err = Err.from("Connection timeout", {
|
|
184
|
+
code: "TIMEOUT",
|
|
185
|
+
metadata: { host: "api.example.com", timeoutMs: 5000 },
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### From native Error
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
static from(error: Error, options?: ErrOptions): Err
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Preserves the original error's:
|
|
196
|
+
- Stack trace (as primary stack for debugging)
|
|
197
|
+
- Cause chain (if `error.cause` is `Error` or `string`)
|
|
198
|
+
- Name (in metadata as `originalName`)
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
try {
|
|
202
|
+
JSON.parse(invalidJson);
|
|
203
|
+
} catch (e) {
|
|
204
|
+
return Err.from(e as Error, { code: "PARSE_ERROR" });
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
#### From another Err (clone with overrides)
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
static from(error: Err, options?: ErrOptions): Err
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
const original = Err.from("Original error");
|
|
216
|
+
const modified = Err.from(original, { code: "NEW_CODE" });
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
#### From unknown value (safe for catch blocks)
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
static from(error: unknown, options?: ErrOptions): Err
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Handles any value that might be thrown, including non-Error objects, strings, numbers, `null`, and `undefined`.
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
try {
|
|
229
|
+
await riskyAsyncOperation();
|
|
230
|
+
} catch (e) {
|
|
231
|
+
// Safe - handles any thrown value
|
|
232
|
+
return Err.from(e).wrap("Operation failed");
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### `Err.wrap()`
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
static wrap(
|
|
240
|
+
message: string,
|
|
241
|
+
error: Err | Error | string,
|
|
242
|
+
options?: ErrOptions
|
|
243
|
+
): Err
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Static convenience method to wrap an error with a context message. Creates a new `Err` with the provided message, having the original error as its cause. Recommended pattern for catch blocks.
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
// Basic usage in catch block
|
|
250
|
+
try {
|
|
251
|
+
await db.query(sql);
|
|
252
|
+
} catch (e) {
|
|
253
|
+
return Err.wrap("Database query failed", e as Error);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// With code and metadata
|
|
257
|
+
try {
|
|
258
|
+
const user = await fetchUser(id);
|
|
259
|
+
} catch (e) {
|
|
260
|
+
return Err.wrap("Failed to fetch user", e as Error, {
|
|
261
|
+
code: "USER_FETCH_ERROR",
|
|
262
|
+
metadata: { userId: id },
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### `Err.aggregate()`
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
static aggregate(
|
|
271
|
+
message: string,
|
|
272
|
+
errors?: Array<Err | Error | string>,
|
|
273
|
+
options?: ErrOptions
|
|
274
|
+
): Err
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Create an aggregate error for collecting multiple errors. Useful for validation, batch operations, or any scenario where multiple errors should be collected and reported together. Defaults to code `"AGGREGATE"`.
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
// Validation
|
|
281
|
+
function validate(data: Input): [Valid, null] | [null, Err] {
|
|
282
|
+
let errors = Err.aggregate("Validation failed");
|
|
283
|
+
|
|
284
|
+
if (!data.email) errors = errors.add("Email is required");
|
|
285
|
+
if (!data.name) errors = errors.add("Name is required");
|
|
286
|
+
|
|
287
|
+
if (errors.count > 0) {
|
|
288
|
+
return [null, errors.withCode("VALIDATION_ERROR")];
|
|
289
|
+
}
|
|
290
|
+
return [data as Valid, null];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Batch operations
|
|
294
|
+
async function processAll(items: Item[]): [null, Err] | [void, null] {
|
|
295
|
+
let errors = Err.aggregate("Batch processing failed");
|
|
296
|
+
|
|
297
|
+
for (const item of items) {
|
|
298
|
+
const [, err] = await processItem(item);
|
|
299
|
+
if (err) {
|
|
300
|
+
errors = errors.add(err.withMetadata({ itemId: item.id }));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (errors.count > 0) return [null, errors];
|
|
305
|
+
return [undefined, null];
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### `Err.fromJSON()`
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
static fromJSON(json: unknown): Err
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Deserialize an `Err` from JSON representation. Reconstructs an `Err` instance from its JSON form, including cause chains and aggregated errors. Validates the input structure.
|
|
316
|
+
|
|
317
|
+
Throws `Error` if json is invalid or missing required fields.
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
// API response handling
|
|
321
|
+
const response = await fetch("/api/users/123");
|
|
322
|
+
if (!response.ok) {
|
|
323
|
+
const body = await response.json();
|
|
324
|
+
if (body.error) {
|
|
325
|
+
const err = Err.fromJSON(body.error);
|
|
326
|
+
if (err.hasCode("NOT_FOUND")) {
|
|
327
|
+
return showNotFound();
|
|
328
|
+
}
|
|
329
|
+
return showError(err);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Message queue processing
|
|
334
|
+
queue.on("error", (message) => {
|
|
335
|
+
const err = Err.fromJSON(message.payload);
|
|
336
|
+
logger.error("Task failed", { error: err.toJSON() });
|
|
337
|
+
});
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### `Err.isErr()`
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
static isErr(value: unknown): value is Err
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Type guard to check if a value is an `Err` instance. Useful for checking values from external sources where `instanceof` may not work (different realms, serialization).
|
|
347
|
+
|
|
348
|
+
Checks for: `instanceof Err`, or presence of `isErr === true` or `kind === "Err"` properties.
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
function handleApiResponse(data: unknown): void {
|
|
352
|
+
if (Err.isErr(data)) {
|
|
353
|
+
console.error("Received error:", data.message);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
// Process data...
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## Wrapping & Context
|
|
361
|
+
|
|
362
|
+
### `wrap()`
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
wrap(context: string | ErrOptions): Err
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Wrap this error with additional context. Creates a new error that has this error as its cause. The original error is preserved and accessible via `unwrap()` or `chain()`.
|
|
369
|
+
|
|
370
|
+
**Stack trace behavior:** The new wrapper captures a fresh stack trace pointing to where `wrap()` was called. The original error's stack is preserved and accessible via `err.unwrap()?.stack` or `err.root.stack`.
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
// Simple wrapping
|
|
374
|
+
const dbErr = queryDatabase();
|
|
375
|
+
if (Err.isErr(dbErr)) {
|
|
376
|
+
return dbErr.wrap("Failed to fetch user");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Wrapping with full options
|
|
380
|
+
return originalErr.wrap({
|
|
381
|
+
message: "Service unavailable",
|
|
382
|
+
code: "SERVICE_ERROR",
|
|
383
|
+
metadata: { service: "user-service", retryAfter: 30 },
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// Accessing original stack
|
|
387
|
+
const wrapped = original.wrap("Context 1").wrap("Context 2");
|
|
388
|
+
console.log(wrapped.stack); // Points to second wrap() call
|
|
389
|
+
console.log(wrapped.root.stack); // Points to original error location
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### `withCode()`
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
withCode(code: ErrCode): Err
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
Create a new `Err` with a different or added error code. Preserves the original stack trace and timestamp.
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
const err = Err.from("Record not found").withCode("NOT_FOUND");
|
|
402
|
+
|
|
403
|
+
if (err.code === "NOT_FOUND") {
|
|
404
|
+
return res.status(404).json(err.toJSON());
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### `withMetadata()`
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
withMetadata(metadata: Record<string, unknown>): Err
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Create a new `Err` with additional metadata. New metadata is merged with existing metadata. Preserves the original stack trace and timestamp.
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
const err = Err.from("Request failed")
|
|
418
|
+
.withMetadata({ url: "/api/users" })
|
|
419
|
+
.withMetadata({ statusCode: 500, retryable: true });
|
|
420
|
+
|
|
421
|
+
console.log(err.metadata);
|
|
422
|
+
// { url: '/api/users', statusCode: 500, retryable: true }
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
## Aggregate Operations
|
|
426
|
+
|
|
427
|
+
### `add()`
|
|
428
|
+
|
|
429
|
+
```typescript
|
|
430
|
+
add(error: Err | Error | string): Err
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Add an error to this aggregate. Returns a new `Err` with the error added (immutable).
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
let errors = Err.aggregate("Form validation failed");
|
|
437
|
+
|
|
438
|
+
if (!email) {
|
|
439
|
+
errors = errors.add("Email is required");
|
|
440
|
+
}
|
|
441
|
+
if (!password) {
|
|
442
|
+
errors = errors.add(
|
|
443
|
+
Err.from("Password is required").withCode("MISSING_PASSWORD"),
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### `addAll()`
|
|
449
|
+
|
|
450
|
+
```typescript
|
|
451
|
+
addAll(errors: Array<Err | Error | string>): Err
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
Add multiple errors to this aggregate at once. Returns a new `Err` with all errors added (immutable).
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
const validationErrors = [
|
|
458
|
+
"Name too short",
|
|
459
|
+
Err.from("Invalid email format").withCode("INVALID_EMAIL"),
|
|
460
|
+
new Error("Age must be positive"),
|
|
461
|
+
];
|
|
462
|
+
|
|
463
|
+
const aggregate = Err.aggregate("Validation failed").addAll(validationErrors);
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
## Inspection
|
|
467
|
+
|
|
468
|
+
### `isAggregate`
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
get isAggregate(): boolean
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Whether this error is an aggregate containing multiple errors.
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
const single = Err.from("Single error");
|
|
478
|
+
const multi = Err.aggregate("Multiple").add("One").add("Two");
|
|
479
|
+
|
|
480
|
+
console.log(single.isAggregate); // false
|
|
481
|
+
console.log(multi.isAggregate); // true
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### `count`
|
|
485
|
+
|
|
486
|
+
```typescript
|
|
487
|
+
get count(): number
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Total count of errors (including nested aggregates). For single errors, returns `1`. For aggregates, recursively counts all child errors.
|
|
491
|
+
|
|
492
|
+
```typescript
|
|
493
|
+
const single = Err.from("One error");
|
|
494
|
+
console.log(single.count); // 1
|
|
495
|
+
|
|
496
|
+
const nested = Err.aggregate("Parent")
|
|
497
|
+
.add("Error 1")
|
|
498
|
+
.add(Err.aggregate("Child").add("Error 2").add("Error 3"));
|
|
499
|
+
|
|
500
|
+
console.log(nested.count); // 3
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### `errors`
|
|
504
|
+
|
|
505
|
+
```typescript
|
|
506
|
+
get errors(): ReadonlyArray<Err>
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
Direct child errors (for aggregates). Returns an empty array for non-aggregate errors.
|
|
510
|
+
|
|
511
|
+
```typescript
|
|
512
|
+
const aggregate = Err.aggregate("Batch failed")
|
|
513
|
+
.add("Task 1 failed")
|
|
514
|
+
.add("Task 2 failed");
|
|
515
|
+
|
|
516
|
+
for (const err of aggregate.errors) {
|
|
517
|
+
console.log(err.message);
|
|
518
|
+
}
|
|
519
|
+
// "Task 1 failed"
|
|
520
|
+
// "Task 2 failed"
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### `root`
|
|
524
|
+
|
|
525
|
+
```typescript
|
|
526
|
+
get root(): Err
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
The root/original error in a wrapped error chain. Follows the cause chain to find the deepest error. Returns `this` if there is no cause.
|
|
530
|
+
|
|
531
|
+
```typescript
|
|
532
|
+
const root = Err.from("Original error");
|
|
533
|
+
const wrapped = root.wrap("Added context").wrap("More context");
|
|
534
|
+
|
|
535
|
+
console.log(wrapped.message); // "More context"
|
|
536
|
+
console.log(wrapped.root.message); // "Original error"
|
|
537
|
+
console.log(wrapped.root.stack); // Stack pointing to original error
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### `unwrap()`
|
|
541
|
+
|
|
542
|
+
```typescript
|
|
543
|
+
unwrap(): Err | undefined
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
Get the directly wrapped error (one level up). Returns `undefined` if this error has no cause.
|
|
547
|
+
|
|
548
|
+
```typescript
|
|
549
|
+
const inner = Err.from("DB connection failed");
|
|
550
|
+
const outer = inner.wrap("Could not save user");
|
|
551
|
+
|
|
552
|
+
const unwrapped = outer.unwrap();
|
|
553
|
+
console.log(unwrapped?.message); // "DB connection failed"
|
|
554
|
+
console.log(inner.unwrap()); // undefined
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### `chain()`
|
|
558
|
+
|
|
559
|
+
```typescript
|
|
560
|
+
chain(): Err[]
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
Get the full chain of wrapped errors from root to current. The first element is the root/original error, the last is `this`.
|
|
564
|
+
|
|
565
|
+
Time complexity: O(n) where n is the depth of the cause chain.
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
const chain = Err.from("Network timeout")
|
|
569
|
+
.wrap("API request failed")
|
|
570
|
+
.wrap("Could not refresh token")
|
|
571
|
+
.wrap("Authentication failed")
|
|
572
|
+
.chain();
|
|
573
|
+
|
|
574
|
+
console.log(chain.map((e) => e.message));
|
|
575
|
+
// [
|
|
576
|
+
// "Network timeout",
|
|
577
|
+
// "API request failed",
|
|
578
|
+
// "Could not refresh token",
|
|
579
|
+
// "Authentication failed",
|
|
580
|
+
// ]
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### `flatten()`
|
|
584
|
+
|
|
585
|
+
```typescript
|
|
586
|
+
flatten(): Err[]
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
Flatten all errors into a single array. For aggregates, recursively collects all leaf errors. For single errors, returns an array containing just this error.
|
|
590
|
+
|
|
591
|
+
Time complexity: O(n) where n is the total number of errors in all nested aggregates.
|
|
592
|
+
|
|
593
|
+
```typescript
|
|
594
|
+
const nested = Err.aggregate("All errors")
|
|
595
|
+
.add("Error A")
|
|
596
|
+
.add(Err.aggregate("Group B").add("Error B1").add("Error B2"))
|
|
597
|
+
.add("Error C");
|
|
598
|
+
|
|
599
|
+
const flat = nested.flatten();
|
|
600
|
+
console.log(flat.map((e) => e.message));
|
|
601
|
+
// ["Error A", "Error B1", "Error B2", "Error C"]
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
## Matching & Filtering
|
|
605
|
+
|
|
606
|
+
### `hasCode()`
|
|
607
|
+
|
|
608
|
+
```typescript
|
|
609
|
+
hasCode(code: ErrCode): boolean
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
Check if this error or any error in its chain/aggregate has a specific code. Searches the cause chain and all aggregated errors.
|
|
613
|
+
|
|
614
|
+
```typescript
|
|
615
|
+
const err = Err.from("DB error", "DB_ERROR")
|
|
616
|
+
.wrap("Repository failed")
|
|
617
|
+
.wrap("Service unavailable");
|
|
618
|
+
|
|
619
|
+
console.log(err.hasCode("DB_ERROR")); // true
|
|
620
|
+
console.log(err.hasCode("NETWORK_ERROR")); // false
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
### `hasCodePrefix()`
|
|
624
|
+
|
|
625
|
+
```typescript
|
|
626
|
+
hasCodePrefix(prefix: string, boundary?: string): boolean
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
Check if this error or any error in its chain/aggregate has a code matching the given prefix with boundary awareness. Default boundary is `":"`.
|
|
630
|
+
|
|
631
|
+
This enables hierarchical error code patterns like `AUTH:TOKEN:EXPIRED`.
|
|
632
|
+
|
|
633
|
+
Matches if:
|
|
634
|
+
- Code equals prefix exactly (e.g., `"AUTH"` matches `"AUTH"`)
|
|
635
|
+
- Code starts with prefix + boundary (e.g., `"AUTH"` matches `"AUTH:EXPIRED"`)
|
|
636
|
+
|
|
637
|
+
Does **NOT** match partial strings (e.g., `"AUTH"` does **NOT** match `"AUTHORIZATION"`).
|
|
638
|
+
|
|
639
|
+
```typescript
|
|
640
|
+
const err = Err.from("Token expired", { code: "AUTH:TOKEN:EXPIRED" });
|
|
641
|
+
|
|
642
|
+
err.hasCodePrefix("AUTH"); // true (matches AUTH:*)
|
|
643
|
+
err.hasCodePrefix("AUTH:TOKEN"); // true (matches AUTH:TOKEN:*)
|
|
644
|
+
err.hasCodePrefix("AUTHORIZATION"); // false (no boundary match)
|
|
645
|
+
|
|
646
|
+
// Custom boundary
|
|
647
|
+
const err2 = Err.from("Not found", { code: "HTTP.404.NOT_FOUND" });
|
|
648
|
+
err2.hasCodePrefix("HTTP", "."); // true
|
|
649
|
+
err2.hasCodePrefix("HTTP.404", "."); // true
|
|
650
|
+
err2.hasCodePrefix("HTTP", ":"); // false (wrong boundary)
|
|
651
|
+
|
|
652
|
+
// Search in error tree
|
|
653
|
+
const err3 = Err.from("DB error", { code: "DB:CONNECTION" })
|
|
654
|
+
.wrap("Service failed", { code: "SERVICE:UNAVAILABLE" });
|
|
655
|
+
|
|
656
|
+
err3.hasCodePrefix("DB"); // true (found in cause)
|
|
657
|
+
err3.hasCodePrefix("SERVICE"); // true (found in current)
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
### `find()`
|
|
661
|
+
|
|
662
|
+
```typescript
|
|
663
|
+
find(predicate: (e: Err) => boolean): Err | undefined
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
Find the first error matching a predicate. Searches this error, its cause chain, and all aggregated errors.
|
|
667
|
+
|
|
668
|
+
```typescript
|
|
669
|
+
const err = Err.aggregate("Multiple failures")
|
|
670
|
+
.add(Err.from("Not found", "NOT_FOUND"))
|
|
671
|
+
.add(Err.from("Timeout", "TIMEOUT"));
|
|
672
|
+
|
|
673
|
+
const timeout = err.find((e) => e.code === "TIMEOUT");
|
|
674
|
+
console.log(timeout?.message); // "Timeout"
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
### `filter()`
|
|
678
|
+
|
|
679
|
+
```typescript
|
|
680
|
+
filter(predicate: (e: Err) => boolean): Err[]
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
Find all errors matching a predicate. Searches this error, its cause chain, and all aggregated errors.
|
|
684
|
+
|
|
685
|
+
```typescript
|
|
686
|
+
const err = Err.aggregate("Validation failed")
|
|
687
|
+
.add(Err.from("Name required", "REQUIRED"))
|
|
688
|
+
.add(Err.from("Invalid email", "INVALID"))
|
|
689
|
+
.add(Err.from("Age required", "REQUIRED"));
|
|
690
|
+
|
|
691
|
+
const required = err.filter((e) => e.code === "REQUIRED");
|
|
692
|
+
console.log(required.length); // 2
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
## Conversion
|
|
696
|
+
|
|
697
|
+
### `toJSON()`
|
|
698
|
+
|
|
699
|
+
```typescript
|
|
700
|
+
toJSON(options?: ErrJSONOptions): ErrJSON
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
Convert to a JSON-serializable object. Use options to control what's included (e.g., omit stack for public APIs).
|
|
704
|
+
|
|
705
|
+
```typescript
|
|
706
|
+
// Full serialization (default)
|
|
707
|
+
const err = Err.from("Not found", {
|
|
708
|
+
code: "NOT_FOUND",
|
|
709
|
+
metadata: { userId: "123" },
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
console.log(JSON.stringify(err.toJSON(), null, 2));
|
|
713
|
+
// {
|
|
714
|
+
// "message": "Not found",
|
|
715
|
+
// "code": "NOT_FOUND",
|
|
716
|
+
// "metadata": { "userId": "123" },
|
|
717
|
+
// "timestamp": "2024-01-15T10:30:00.000Z",
|
|
718
|
+
// "stack": "Error: ...",
|
|
719
|
+
// "errors": []
|
|
720
|
+
// }
|
|
721
|
+
|
|
722
|
+
// Public API response (no stack)
|
|
723
|
+
app.get("/user/:id", (req, res) => {
|
|
724
|
+
const result = getUser(req.params.id);
|
|
725
|
+
if (Err.isErr(result)) {
|
|
726
|
+
const status = result.code === "NOT_FOUND" ? 404 : 500;
|
|
727
|
+
return res.status(status).json({
|
|
728
|
+
error: result.toJSON({ stack: false }),
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
res.json(result);
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
// Minimal payload
|
|
735
|
+
err.toJSON({ stack: false, metadata: false });
|
|
736
|
+
// Only includes: message, code, timestamp, cause, errors
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
### `toString()`
|
|
740
|
+
|
|
741
|
+
```typescript
|
|
742
|
+
toString(options?: ToStringOptions): string
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
Convert to a formatted string for logging/display. Includes cause chain and aggregated errors with indentation.
|
|
746
|
+
|
|
747
|
+
```typescript
|
|
748
|
+
// Basic usage (no options)
|
|
749
|
+
const err = Err.from("DB error")
|
|
750
|
+
.wrap("Repository failed")
|
|
751
|
+
.wrap("Service unavailable");
|
|
752
|
+
|
|
753
|
+
console.log(err.toString());
|
|
754
|
+
// [ERROR] Service unavailable
|
|
755
|
+
// Caused by: [ERROR] Repository failed
|
|
756
|
+
// Caused by: [ERROR] DB error
|
|
757
|
+
|
|
758
|
+
// With options
|
|
759
|
+
const err2 = Err.from("Connection failed", {
|
|
760
|
+
code: "DB:CONNECTION",
|
|
761
|
+
metadata: { host: "localhost", port: 5432 },
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
console.log(err2.toString({ date: true, metadata: true, stack: 3 }));
|
|
765
|
+
// [2024-01-15T10:30:00.000Z] [DB:CONNECTION] Connection failed
|
|
766
|
+
// metadata: {"host":"localhost","port":5432}
|
|
767
|
+
// stack:
|
|
768
|
+
// at Database.connect (src/db.ts:45)
|
|
769
|
+
// at Repository.init (src/repo.ts:23)
|
|
770
|
+
// at Service.start (src/service.ts:12)
|
|
771
|
+
|
|
772
|
+
// Aggregate
|
|
773
|
+
const err3 = Err.aggregate("Validation failed", [], { code: "VALIDATION" })
|
|
774
|
+
.add("Name required")
|
|
775
|
+
.add("Email invalid");
|
|
776
|
+
|
|
777
|
+
console.log(err3.toString());
|
|
778
|
+
// [VALIDATION] Validation failed
|
|
779
|
+
// Errors (2):
|
|
780
|
+
// - [ERROR] Name required
|
|
781
|
+
// - [ERROR] Email invalid
|
|
782
|
+
|
|
783
|
+
// With maxDepth limit
|
|
784
|
+
const deep = Err.from("Root")
|
|
785
|
+
.wrap("Level 1")
|
|
786
|
+
.wrap("Level 2")
|
|
787
|
+
.wrap("Level 3");
|
|
788
|
+
|
|
789
|
+
console.log(deep.toString({ maxDepth: 2 }));
|
|
790
|
+
// [ERROR] Level 3
|
|
791
|
+
// Caused by: [ERROR] Level 2
|
|
792
|
+
// ... (2 more causes)
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
### `toError()`
|
|
796
|
+
|
|
797
|
+
```typescript
|
|
798
|
+
toError(): Error
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
Convert to a native `Error` for interop with throw-based APIs.
|
|
802
|
+
|
|
803
|
+
Creates an `Error` with:
|
|
804
|
+
- `message`: This error's message
|
|
805
|
+
- `name`: This error's code (or `"Err"`)
|
|
806
|
+
- `stack`: This error's original stack trace
|
|
807
|
+
- `cause`: Converted cause chain (native `Error`)
|
|
808
|
+
|
|
809
|
+
Note: Metadata is not included on the native `Error`.
|
|
810
|
+
|
|
811
|
+
```typescript
|
|
812
|
+
const err = Err.from("Something failed", "MY_ERROR");
|
|
813
|
+
|
|
814
|
+
// If you need to throw for some API
|
|
815
|
+
throw err.toError();
|
|
816
|
+
|
|
817
|
+
// The thrown error will have:
|
|
818
|
+
// - error.message === "Something failed"
|
|
819
|
+
// - error.name === "MY_ERROR"
|
|
820
|
+
// - error.stack === (original stack trace)
|
|
821
|
+
// - error.cause === (if wrapped)
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
## Usage Patterns
|
|
825
|
+
|
|
826
|
+
### Tuple pattern
|
|
827
|
+
|
|
828
|
+
```typescript
|
|
829
|
+
function divide(a: number, b: number): [number, null] | [null, Err] {
|
|
830
|
+
if (b === 0) {
|
|
831
|
+
return [null, Err.from("Division by zero", "MATH_ERROR")];
|
|
832
|
+
}
|
|
833
|
+
return [a / b, null];
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const [result, err] = divide(10, 0);
|
|
837
|
+
if (err) {
|
|
838
|
+
console.error(err.toString());
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
console.log(result);
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
### Error wrapping with context
|
|
845
|
+
|
|
846
|
+
```typescript
|
|
847
|
+
function readConfig(path: string): [Config, null] | [null, Err] {
|
|
848
|
+
const [content, readErr] = readFile(path);
|
|
849
|
+
if (readErr) {
|
|
850
|
+
return [null, readErr.wrap(`Failed to read config from ${path}`)];
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const [parsed, parseErr] = parseJSON(content);
|
|
854
|
+
if (parseErr) {
|
|
855
|
+
return [
|
|
856
|
+
null,
|
|
857
|
+
parseErr
|
|
858
|
+
.wrap("Invalid config format")
|
|
859
|
+
.withCode("CONFIG_ERROR")
|
|
860
|
+
.withMetadata({ path }),
|
|
861
|
+
];
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
return [parsed as Config, null];
|
|
865
|
+
}
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
### Catching native errors
|
|
869
|
+
|
|
870
|
+
```typescript
|
|
871
|
+
function parseData(raw: string): [Data, null] | [null, Err] {
|
|
872
|
+
try {
|
|
873
|
+
return [JSON.parse(raw), null];
|
|
874
|
+
} catch (e) {
|
|
875
|
+
return [null, Err.wrap("Failed to parse data", e as Error)];
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
### Serialization for service-to-service communication
|
|
881
|
+
|
|
882
|
+
```typescript
|
|
883
|
+
// Backend: serialize error for API response
|
|
884
|
+
const err = Err.from("User not found", "NOT_FOUND");
|
|
885
|
+
res.status(404).json({ error: err.toJSON() });
|
|
886
|
+
|
|
887
|
+
// Frontend: deserialize error from API response
|
|
888
|
+
const response = await fetch("/api/user/123");
|
|
889
|
+
if (!response.ok) {
|
|
890
|
+
const { error } = await response.json();
|
|
891
|
+
const err = Err.fromJSON(error);
|
|
892
|
+
console.log(err.code); // 'NOT_FOUND'
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// Public API: omit stack traces
|
|
896
|
+
res.json({ error: err.toJSON({ stack: false }) });
|
|
897
|
+
```
|