@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/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
+ ```