unthrown 3.0.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,7 +11,10 @@ pnpm add unthrown
11
11
  ```
12
12
 
13
13
  ```ts
14
- import { Ok, Err, fromPromise, type Result } from "unthrown";
14
+ import { fromPromise, TaggedError } from "unthrown";
15
+
16
+ class NotFound extends TaggedError("NotFound") {} // our modeled domain failure
17
+ class NotFoundError extends Error {} // what `fetchUser` rejects with on a 404
15
18
 
16
19
  const user = fromPromise(fetchUser(id), (cause, defect) =>
17
20
  cause instanceof NotFoundError ? new NotFound() : defect(cause),
@@ -37,4 +40,4 @@ and complete API.
37
40
 
38
41
  ## License
39
42
 
40
- [MIT](../../LICENSE) © Benoit TRAVERS
43
+ [MIT](https://github.com/btravstack/unthrown/blob/main/LICENSE) © Benoit TRAVERS
package/dist/index.cjs CHANGED
@@ -15,6 +15,8 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
15
15
  * re-thrown (with its original stack) instead.
16
16
  *
17
17
  * @typeParam E - the type of the {@link UnwrapError.error} it carries.
18
+ *
19
+ * @category Errors
18
20
  */
19
21
  var UnwrapError = class extends Error {
20
22
  /**
@@ -129,7 +131,7 @@ var Res = class {
129
131
  f(this.error);
130
132
  return this;
131
133
  } catch (cause) {
132
- return defectRes(cause);
134
+ return observerThrowToDefect(cause, this.error);
133
135
  }
134
136
  }
135
137
  flatTapErr(f) {
@@ -138,7 +140,7 @@ var Res = class {
138
140
  const r = f(this.error);
139
141
  return r.tag === "Ok" ? this : passThrough(r);
140
142
  } catch (cause) {
141
- return defectRes(cause);
143
+ return observerThrowToDefect(cause, this.error);
142
144
  }
143
145
  }
144
146
  recoverDefect(f) {
@@ -155,7 +157,7 @@ var Res = class {
155
157
  f(this.cause);
156
158
  return this;
157
159
  } catch (cause) {
158
- return defectRes(cause);
160
+ return observerThrowToDefect(cause, this.cause);
159
161
  }
160
162
  }
161
163
  match(cases) {
@@ -218,10 +220,10 @@ const RESULT_PROTO = Res.prototype;
218
220
  * @internal
219
221
  */
220
222
  function okRes(value) {
221
- return Object.assign(Object.create(RESULT_PROTO), {
223
+ return Object.freeze(Object.assign(Object.create(RESULT_PROTO), {
222
224
  tag: "Ok",
223
225
  value
224
- });
226
+ }));
225
227
  }
226
228
  /**
227
229
  * Construct an `Err` result.
@@ -229,10 +231,10 @@ function okRes(value) {
229
231
  * @internal
230
232
  */
231
233
  function errRes(error) {
232
- return Object.assign(Object.create(RESULT_PROTO), {
234
+ return Object.freeze(Object.assign(Object.create(RESULT_PROTO), {
233
235
  tag: "Err",
234
236
  error
235
- });
237
+ }));
236
238
  }
237
239
  /**
238
240
  * Construct a `Defect` result.
@@ -240,10 +242,10 @@ function errRes(error) {
240
242
  * @internal
241
243
  */
242
244
  function defectRes(cause) {
243
- return Object.assign(Object.create(RESULT_PROTO), {
245
+ return Object.freeze(Object.assign(Object.create(RESULT_PROTO), {
244
246
  tag: "Defect",
245
247
  cause
246
- });
248
+ }));
247
249
  }
248
250
  /**
249
251
  * Type guard: is `x` a {@link Result} (any of `Ok` / `Err` / `Defect`)?
@@ -256,6 +258,20 @@ function defectRes(cause) {
256
258
  * is not a `Result` and returns `false`.
257
259
  *
258
260
  * @returns `true` when `x` is a `Result` produced by this library.
261
+ *
262
+ * @example
263
+ * ```ts
264
+ * import { isResult, Ok } from "unthrown";
265
+ *
266
+ * isResult(Ok(1)); // => true
267
+ * isResult({ tag: "Ok" }); // => false (look-alike, wrong prototype)
268
+ * isResult(Ok(1).toAsync()); // => false (an AsyncResult is not a Result)
269
+ *
270
+ * const x: unknown = Ok(1);
271
+ * if (isResult(x)) x.match({ ok: () => 1, err: () => 0, defect: () => -1 });
272
+ * ```
273
+ *
274
+ * @category Guards
259
275
  */
260
276
  function isResult(x) {
261
277
  return x instanceof Res;
@@ -274,6 +290,18 @@ function passThrough(self) {
274
290
  return self;
275
291
  }
276
292
  /**
293
+ * A throw inside a *failure observer* (`tapErr` / `tapDefect` / `flatTapErr`)
294
+ * must not destroy the failure being observed — that is the exact place (e.g. a
295
+ * failing error-logger) where losing the underlying failure hurts most. The
296
+ * resulting Defect aggregates both: `errors[0]` is the observer's throw,
297
+ * `errors[1]` the original failure.
298
+ *
299
+ * @internal
300
+ */
301
+ function observerThrowToDefect(thrown, original) {
302
+ return defectRes(new AggregateError([thrown, original], "unthrown: a failure-observer callback threw; errors[0] is the callback's throw, errors[1] the original failure"));
303
+ }
304
+ /**
277
305
  * Validate that a `bind`/`let` scope is a real (non-null) object before merging a
278
306
  * key into it.
279
307
  *
@@ -292,7 +320,7 @@ function passThrough(self) {
292
320
  * @internal
293
321
  */
294
322
  function scopeOf(value) {
295
- if (typeof value !== "object" || value === null) throw new TypeError("bind/let requires an object scope — start a do-chain with Do()");
323
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("bind/let requires an object scope — start a do-chain with Do()");
296
324
  return value;
297
325
  }
298
326
  /**
@@ -420,7 +448,7 @@ var AsyncRes = class AsyncRes {
420
448
  f(r.error);
421
449
  return r;
422
450
  } catch (cause) {
423
- return defectRes(cause);
451
+ return observerThrowToDefect(cause, r.error);
424
452
  }
425
453
  }));
426
454
  }
@@ -431,7 +459,7 @@ var AsyncRes = class AsyncRes {
431
459
  const inner = await f(r.error);
432
460
  return inner.tag === "Ok" ? passThrough(r) : passThrough(inner);
433
461
  } catch (cause) {
434
- return defectRes(cause);
462
+ return observerThrowToDefect(cause, r.error);
435
463
  }
436
464
  }));
437
465
  }
@@ -452,7 +480,7 @@ var AsyncRes = class AsyncRes {
452
480
  f(r.cause);
453
481
  return r;
454
482
  } catch (cause) {
455
- return defectRes(cause);
483
+ return observerThrowToDefect(cause, r.cause);
456
484
  }
457
485
  }));
458
486
  }
@@ -489,8 +517,12 @@ var AsyncRes = class AsyncRes {
489
517
  * @example
490
518
  * ```ts
491
519
  * import { Ok } from "unthrown";
492
- * Ok(42).unwrap(); // 42
520
+ *
521
+ * Ok(2).map((n) => n + 1); // => Ok(3)
522
+ * Ok(42).unwrap(); // => 42
493
523
  * ```
524
+ *
525
+ * @category Constructors
494
526
  */
495
527
  function Ok(value) {
496
528
  return okRes(value);
@@ -504,8 +536,12 @@ function Ok(value) {
504
536
  * @example
505
537
  * ```ts
506
538
  * import { Err } from "unthrown";
507
- * Err("not_found").unwrapErr(); // "not_found"
539
+ *
540
+ * Err("not_found").map((n) => n + 1); // => Err("not_found") (map skipped)
541
+ * Err("not_found").unwrapErr(); // => "not_found"
508
542
  * ```
543
+ *
544
+ * @category Constructors
509
545
  */
510
546
  function Err(error) {
511
547
  return errRes(error);
@@ -517,10 +553,16 @@ function Err(error) {
517
553
  *
518
554
  * @example
519
555
  * ```ts
520
- * import { isOk, type Result } from "unthrown";
556
+ * import { isOk, Ok, Err, type Result } from "unthrown";
557
+ *
558
+ * isOk(Ok(1)); // => true
559
+ * isOk(Err("boom")); // => false
560
+ *
521
561
  * declare const r: Result<number, string>;
522
562
  * if (isOk(r)) r.value; // number, narrowed
523
563
  * ```
564
+ *
565
+ * @category Guards
524
566
  */
525
567
  function isOk(r) {
526
568
  return r.tag === "Ok";
@@ -529,6 +571,19 @@ function isOk(r) {
529
571
  * Type guard: narrow a {@link Result} to its `Err` variant, exposing `.error`.
530
572
  *
531
573
  * @returns `true` when `r` is `Err`.
574
+ *
575
+ * @example
576
+ * ```ts
577
+ * import { isErr, Ok, Err, type Result } from "unthrown";
578
+ *
579
+ * isErr(Err("boom")); // => true
580
+ * isErr(Ok(1)); // => false
581
+ *
582
+ * declare const r: Result<number, string>;
583
+ * if (isErr(r)) r.error; // string, narrowed
584
+ * ```
585
+ *
586
+ * @category Guards
532
587
  */
533
588
  function isErr(r) {
534
589
  return r.tag === "Err";
@@ -536,7 +591,27 @@ function isErr(r) {
536
591
  /**
537
592
  * Type guard: narrow a {@link Result} to its `Defect` variant, exposing `.cause`.
538
593
  *
594
+ * @remarks
595
+ * A `Defect` has no public constructor — it only arises at a boundary (e.g. a
596
+ * callback throwing inside a combinator). This guard is how you detect one.
597
+ *
539
598
  * @returns `true` when `r` is a `Defect`.
599
+ *
600
+ * @example
601
+ * ```ts
602
+ * import { isDefect, Ok } from "unthrown";
603
+ *
604
+ * // A throw inside a combinator is captured as a Defect:
605
+ * const r = Ok(1).map(() => {
606
+ * throw new Error("boom");
607
+ * });
608
+ * isDefect(r); // => true
609
+ * isDefect(Ok(1)); // => false
610
+ *
611
+ * if (isDefect(r)) r.cause; // unknown, narrowed
612
+ * ```
613
+ *
614
+ * @category Guards
540
615
  */
541
616
  function isDefect(r) {
542
617
  return r.tag === "Defect";
@@ -564,6 +639,24 @@ function isDefect(r) {
564
639
  * .map(({ user, org, label }) => render(user, org, label));
565
640
  * // Result<View, NotFound>
566
641
  * ```
642
+ *
643
+ * @example
644
+ * ```ts
645
+ * import { Do, Ok, Err } from "unthrown";
646
+ *
647
+ * // Ok path — the scope accumulates:
648
+ * Do()
649
+ * .bind("a", () => Ok(2))
650
+ * .let("b", ({ a }) => a * 10)
651
+ * .map(({ a, b }) => a + b); // => Ok(22)
652
+ *
653
+ * // Err path — the first Err short-circuits the rest:
654
+ * Do()
655
+ * .bind("a", () => Err("boom"))
656
+ * .let("b", ({ a }) => a); // => Err("boom")
657
+ * ```
658
+ *
659
+ * @category Do-notation
567
660
  */
568
661
  function Do() {
569
662
  return Ok({});
@@ -613,10 +706,16 @@ function isDefectMarker(x) {
613
706
  * @param value - the possibly-absent value.
614
707
  * @param onAbsent - lazily produces the error for the absent case.
615
708
  *
709
+ * @category Interop
710
+ *
616
711
  * @example
617
712
  * ```ts
618
713
  * import { fromNullable } from "unthrown";
619
- * fromNullable(map.get(key), () => "missing").unwrap();
714
+ *
715
+ * const map = new Map([["a", 1]]);
716
+ * fromNullable(map.get("a"), () => "absent").unwrap(); // => 1
717
+ * fromNullable(map.get("z"), () => "absent"); // => Err("absent")
718
+ * fromNullable(0, () => "absent").unwrap(); // => 0 (falsy but present)
620
719
  * ```
621
720
  */
622
721
  function fromNullable(value, onAbsent) {
@@ -647,11 +746,21 @@ function fromNullable(value, onAbsent) {
647
746
  * unmodeled by returning `defect(cause)` (the helper passed as its second arg).
648
747
  * @returns a function with the same arguments returning `Result<T, E>`.
649
748
  *
749
+ * @category Interop
750
+ *
650
751
  * @example
651
752
  * ```ts
652
753
  * import { fromThrowable } from "unthrown";
653
- * const parse = fromThrowable(JSON.parse, (cause, defect) => defect(cause));
654
- * parse("{}").unwrap();
754
+ *
755
+ * // Model the parse failure as an `Err`, everything unexpected as a `Defect`.
756
+ * const parse = fromThrowable(
757
+ * (text: string) => JSON.parse(text) as unknown,
758
+ * (cause, defect) =>
759
+ * cause instanceof SyntaxError ? ("invalid_json" as const) : defect(cause),
760
+ * );
761
+ *
762
+ * parse('{"ok":true}').unwrap(); // => { ok: true }
763
+ * parse("nope"); // => Err("invalid_json")
655
764
  * ```
656
765
  */
657
766
  function fromThrowable(fn, qualify) {
@@ -686,17 +795,24 @@ function fromThrowable(fn, qualify) {
686
795
  * @param qualify - triages a rejection `cause` into a modeled `E`, or marks it
687
796
  * unmodeled by returning `defect(cause)` (the helper passed as its second arg).
688
797
  *
798
+ * @category Interop
799
+ *
689
800
  * @example
690
801
  * ```ts
691
802
  * import { fromPromise } from "unthrown";
803
+ *
804
+ * // A rejection with a NotFoundError becomes a modeled `Err`; anything else a Defect.
692
805
  * const user = await fromPromise(fetchUser(id), (cause, defect) =>
693
806
  * cause instanceof NotFoundError ? ("not_found" as const) : defect(cause),
694
807
  * );
808
+ *
809
+ * user.unwrap(); // => the fetched user (on success)
810
+ * // when fetchUser rejects with NotFoundError: => Err("not_found")
695
811
  * ```
696
812
  */
697
813
  function fromPromise(promise, qualify) {
698
814
  const triage = qualify;
699
- return new AsyncRes((typeof promise === "function" ? Promise.resolve().then(promise) : promise).then((value) => okRes(value), (cause) => qualifyToResult(cause, triage)));
815
+ return new AsyncRes((typeof promise === "function" ? Promise.resolve().then(promise) : Promise.resolve(promise)).then((value) => okRes(value), (cause) => qualifyToResult(cause, triage)));
700
816
  }
701
817
  /**
702
818
  * Wrap a `Promise` asserted **not** to fail in any modeled way: any rejection
@@ -709,9 +825,20 @@ function fromPromise(promise, qualify) {
709
825
  *
710
826
  * @typeParam T - the resolved value type.
711
827
  * @param promise - the promise, or a thunk returning one.
828
+ *
829
+ * @category Interop
830
+ *
831
+ * @example
832
+ * ```ts
833
+ * import { fromSafePromise } from "unthrown";
834
+ *
835
+ * (await fromSafePromise(Promise.resolve(3))).unwrap(); // => 3
836
+ * // a rejection becomes a Defect (never a modeled Err):
837
+ * await fromSafePromise(Promise.reject(new Error("boom"))); // => Defect(Error("boom"))
838
+ * ```
712
839
  */
713
840
  function fromSafePromise(promise) {
714
- return new AsyncRes((typeof promise === "function" ? Promise.resolve().then(promise) : promise).then((value) => okRes(value), (cause) => defectRes(cause)));
841
+ return new AsyncRes((typeof promise === "function" ? Promise.resolve().then(promise) : Promise.resolve(promise)).then((value) => okRes(value), (cause) => defectRes(cause)));
715
842
  }
716
843
  function qualifyToResult(cause, qualify) {
717
844
  try {
@@ -749,8 +876,10 @@ function foldRecord(results) {
749
876
  let firstErr;
750
877
  let firstDefect;
751
878
  const values = {};
752
- for (const [key, r] of Object.entries(results)) if (r.tag === "Defect") firstDefect ??= r;
753
- else if (r.tag === "Err") firstErr ??= r;
879
+ for (const [key, r] of Object.entries(results)) if (r.tag === "Defect") {
880
+ firstDefect ??= r;
881
+ break;
882
+ } else if (r.tag === "Err") firstErr ??= r;
754
883
  else Object.defineProperty(values, key, {
755
884
  value: r.value,
756
885
  enumerable: true,
@@ -771,11 +900,14 @@ function foldRecord(results) {
771
900
  * collapses to `Result<T[], E>` with no cast. For a **record** keyed by name,
772
901
  * use {@link allFromDict}.
773
902
  *
903
+ * @category Aggregate
904
+ *
774
905
  * @example
775
906
  * ```ts
776
- * import { all, Ok } from "unthrown";
777
- * all([Ok(1), Ok("a"), Ok(true)]).unwrap(); // [1, "a", true] (typed [number, string, boolean])
778
- * all([Ok(1), Ok(2)] as Result<number, never>[]).unwrap(); // number[]
907
+ * import { all, Ok, Err } from "unthrown";
908
+ *
909
+ * all([Ok(1), Ok("a"), Ok(true)]).unwrap(); // => [1, "a", true] (typed [number, string, boolean])
910
+ * all([Ok(1), Err("e"), Ok(3)]); // => Err("e") (short-circuits on the first Err)
779
911
  * ```
780
912
  */
781
913
  function all(results) {
@@ -791,10 +923,14 @@ function all(results) {
791
923
  * Same folding rules as {@link all}: first `Err` short-circuits, any `Defect`
792
924
  * dominates. This is **not** error accumulation.
793
925
  *
926
+ * @category Aggregate
927
+ *
794
928
  * @example
795
929
  * ```ts
796
- * import { allFromDict, Ok } from "unthrown";
797
- * allFromDict({ id: Ok(1), name: Ok("ada") }).unwrap(); // { id: 1, name: "ada" }
930
+ * import { allFromDict, Ok, Err } from "unthrown";
931
+ *
932
+ * allFromDict({ id: Ok(1), name: Ok("ada") }).unwrap(); // => { id: 1, name: "ada" }
933
+ * allFromDict({ id: Ok(1), name: Err("missing") }); // => Err("missing")
798
934
  * ```
799
935
  */
800
936
  function allFromDict(results) {
@@ -810,10 +946,14 @@ function allFromDict(results) {
810
946
  * short-circuits, any `Defect` dominates. As ever, the returned `AsyncResult`'s
811
947
  * internal promise never rejects. For a **record**, use {@link allFromDictAsync}.
812
948
  *
949
+ * @category Aggregate
950
+ *
813
951
  * @example
814
952
  * ```ts
815
953
  * import { allAsync, fromSafePromise } from "unthrown";
816
- * await allAsync([fromSafePromise(a()), fromSafePromise(b())]);
954
+ *
955
+ * const both = allAsync([fromSafePromise(Promise.resolve(1)), fromSafePromise(Promise.resolve(2))]);
956
+ * (await both).unwrap(); // => [1, 2]
817
957
  * ```
818
958
  */
819
959
  function allAsync(results) {
@@ -827,10 +967,17 @@ function allAsync(results) {
827
967
  * Resolved concurrently (order preserved), folded with the {@link all} rules,
828
968
  * and the internal promise never rejects.
829
969
  *
970
+ * @category Aggregate
971
+ *
830
972
  * @example
831
973
  * ```ts
832
974
  * import { allFromDictAsync, fromSafePromise } from "unthrown";
833
- * await allFromDictAsync({ a: fromSafePromise(a()), b: fromSafePromise(b()) });
975
+ *
976
+ * const both = allFromDictAsync({
977
+ * a: fromSafePromise(Promise.resolve(1)),
978
+ * b: fromSafePromise(Promise.resolve("x")),
979
+ * });
980
+ * (await both).unwrap(); // => { a: 1, b: "x" }
834
981
  * ```
835
982
  */
836
983
  function allFromDictAsync(results) {
@@ -862,10 +1009,12 @@ function allFromDictAsync(results) {
862
1009
  * (`AsyncResult.fromPromise`, `AsyncResult.all`, …), grouped by what they
863
1010
  * return — a static lives in exactly one namespace.
864
1011
  *
1012
+ * @category Facade
1013
+ *
865
1014
  * @example
866
1015
  * ```ts
867
1016
  * import { Result } from "unthrown";
868
- * Result.Ok(1).flatMap((n) => Result.Ok(n + 1)).unwrap(); // 2
1017
+ * Result.Ok(1).flatMap((n) => Result.Ok(n + 1)).unwrap(); // => 2
869
1018
  * ```
870
1019
  */
871
1020
  const Result = {
@@ -896,10 +1045,13 @@ const Result = {
896
1045
  * {@link Result}, the free functions remain the primary, tree-shakeable API; the
897
1046
  * value `AsyncResult` and the type {@link AsyncResult} share one name.
898
1047
  *
1048
+ * @category Facade
1049
+ *
899
1050
  * @example
900
1051
  * ```ts
901
1052
  * import { AsyncResult } from "unthrown";
902
1053
  * const user = await AsyncResult.fromPromise(fetchUser(id), (c, defect) => defect(c));
1054
+ * user.unwrap(); // => the fetched user (on success)
903
1055
  * ```
904
1056
  */
905
1057
  const AsyncResult = {
@@ -918,7 +1070,10 @@ const AsyncResult = {
918
1070
  * Extend the returned class to declare a concrete error. Supply the payload with
919
1071
  * an instantiation expression; omit it for a payload-less error. A `message`
920
1072
  * field in the payload is forwarded to `Error`. The `_tag` always reflects
921
- * `tag` and cannot be overridden by the payload.
1073
+ * `tag` and cannot be overridden by the payload. `name` is likewise reserved —
1074
+ * it is the display label (set it with `options.name`); a payload `name` is
1075
+ * rejected at compile time (and excluded from the instance type), so it can't
1076
+ * shadow `Error.name`.
922
1077
  *
923
1078
  * `_tag` is the discriminant used by {@link matchTags}; `Error.name` is the
924
1079
  * human-facing label in stack traces and logs. By default they coincide, but
@@ -941,13 +1096,15 @@ const AsyncResult = {
941
1096
  * @param options - optional overrides. `options.name` sets `Error.name`
942
1097
  * independently of `tag` (defaults to `tag`).
943
1098
  *
1099
+ * @category Tagged errors
1100
+ *
944
1101
  * @example
945
1102
  * ```ts
946
1103
  * class NotFound extends TaggedError("NotFound") {}
947
1104
  * class HttpError extends TaggedError("HttpError")<{ status: number }> {}
948
1105
  *
949
- * new NotFound()._tag; // "NotFound"
950
- * new HttpError({ status: 500 }).status; // 500
1106
+ * new NotFound()._tag; // => "NotFound"
1107
+ * new HttpError({ status: 500 }).status; // => 500
951
1108
  * ```
952
1109
  */
953
1110
  function TaggedError(tag, options) {
@@ -966,8 +1123,9 @@ function TaggedError(tag, options) {
966
1123
  }
967
1124
  function matchTags(result, handlers) {
968
1125
  const onErr = (error) => {
969
- const handler = handlers[error._tag];
970
- return handler(error);
1126
+ const tag = error._tag;
1127
+ const handler = tag === "Ok" || tag === "Defect" || !Object.hasOwn(handlers, tag) ? void 0 : handlers[tag];
1128
+ return handler ? handler(error) : handlers.Defect(error);
971
1129
  };
972
1130
  return result.match({
973
1131
  ok: handlers.Ok,