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