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