unthrown 5.8.0 → 5.10.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/dist/index.cjs CHANGED
@@ -18,24 +18,63 @@ const PATTERN_BRAND = Symbol.for("unthrown.matcher.pattern");
18
18
  * is a bug).
19
19
  *
20
20
  * @category Errors
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * import { match, NonExhaustiveError } from "unthrown";
25
+ *
26
+ * // A value typed "a" | "b" that is really "c" (a cast, a raw-JS caller):
27
+ * const rogue = "c" as "a" | "b";
28
+ * try {
29
+ * match(rogue)
30
+ * .with("a", () => 1)
31
+ * .with("b", () => 2)
32
+ * .exhaustive();
33
+ * } catch (error) {
34
+ * error instanceof NonExhaustiveError; // => true
35
+ * (error as NonExhaustiveError).input; // => "c"
36
+ * }
37
+ * ```
21
38
  */
22
39
  var NonExhaustiveError = class extends Error {
23
40
  /** The value no arm matched. */
24
41
  input;
25
42
  constructor(input) {
26
- let printed;
27
- try {
28
- printed = JSON.stringify(input) ?? String(input);
29
- } catch {
30
- printed = String(input);
31
- }
32
- super(`unthrown: no pattern matched the value ${printed}`);
43
+ super(`unthrown: no pattern matched the value ${printValue(input)}`);
33
44
  this.name = "NonExhaustiveError";
34
45
  this.input = input;
35
46
  Object.setPrototypeOf(this, new.target.prototype);
36
47
  }
37
48
  };
38
49
  /**
50
+ * Render a rogue value for {@link NonExhaustiveError}'s message — **total**,
51
+ * because the error is constructed inside the throw → defect net and a throw
52
+ * here would replace the diagnostic with an unrelated one (or escape `match`).
53
+ *
54
+ * @remarks
55
+ * Each step can fail for a different input, so each is guarded: `JSON.stringify`
56
+ * RETURNS undefined for a function, a symbol or `undefined` and throws for a
57
+ * bigint or a circular object; `String()` throws for a null-prototype object or
58
+ * a hostile `toString` / `Symbol.toPrimitive`; `Object.prototype.toString`
59
+ * throws only for a Proxy whose `get` trap does. The last resort is a constant.
60
+ *
61
+ * @internal
62
+ */
63
+ function printValue(input) {
64
+ try {
65
+ const json = JSON.stringify(input);
66
+ if (json !== void 0) return json;
67
+ } catch {}
68
+ try {
69
+ return String(input);
70
+ } catch {}
71
+ try {
72
+ return Object.prototype.toString.call(input);
73
+ } catch {
74
+ return "<unprintable value>";
75
+ }
76
+ }
77
+ /**
39
78
  * Is `x` a *plain* object (prototype `Object.prototype` or `null`) — an object
40
79
  * literal, the only object shape that acts as a structural pattern?
41
80
  *
@@ -123,6 +162,24 @@ Object.freeze(MatcherImpl.prototype);
123
162
  * {@link P}).
124
163
  *
125
164
  * @category Constructors
165
+ *
166
+ * @example
167
+ * ```ts
168
+ * import { match, type Result } from "unthrown";
169
+ *
170
+ * // Matching a whole Result natively — every variant named, `.exhaustive()` last:
171
+ * declare const r: Result<number, "odd" | "negative">;
172
+ * const label = match(r)
173
+ * .with({ tag: "Ok" }, (ok) => `got ${ok.value}`)
174
+ * .with({ tag: "Err" }, (err) => `failed: ${err.error}`)
175
+ * .with({ tag: "Defect" }, () => "bug")
176
+ * .exhaustive();
177
+ *
178
+ * // Inside a combinator, return the un-terminated builder — it runs `.exhaustive()`:
179
+ * const reason = r.mapErrCases((matcher) =>
180
+ * matcher.with("odd", () => "not even" as const).with("negative", () => "below zero" as const),
181
+ * ); // Result<number, "not even" | "below zero">
182
+ * ```
126
183
  */
127
184
  function match(value) {
128
185
  return new MatcherImpl(value);
@@ -156,17 +213,49 @@ const universal = pattern(() => true);
156
213
  * (`.with(P.tag("A"), P.tag("B"), handler)`).
157
214
  * - `P.instanceOf(Cls)` — an `instanceof` check, narrowing to the class
158
215
  * instance type (for union members that are not tagged, e.g. a third-party
159
- * error class).
216
+ * error class). **Exhaustiveness here is structural, the check is not:**
217
+ * two classes with the same shape (`class A extends Error {}`,
218
+ * `class B extends Error {}`) are one type to the compiler, so a match
219
+ * naming only `A` compiles as exhaustive while a `B` fails `instanceof A`
220
+ * at runtime and becomes a `Defect`. Give each class a distinguishing field
221
+ * (a `readonly kind = "A"` literal) or use `TaggedError`, and the missing
222
+ * arm is a compile error again.
160
223
  * - `P.when(guard)` — an arbitrary type-guard predicate. Also the way to match
161
224
  * a primitive shape (`P.when((v): v is string => typeof v === "string")`),
162
225
  * and grouping patterns under one handler is what a `.with(a, b, handler)`
163
226
  * arm already does.
164
227
  *
165
228
  * @category Constructors
229
+ *
230
+ * @example
231
+ * ```ts
232
+ * import { P, TaggedError, type Result } from "unthrown";
233
+ *
234
+ * class NotFound extends TaggedError("NotFound")<{ id: string }> {}
235
+ * class Conflict extends TaggedError("Conflict") {}
236
+ * class VendorTimeout extends Error {
237
+ * readonly afterMs = 30_000;
238
+ * }
239
+ *
240
+ * declare const r: Result<string, NotFound | Conflict | VendorTimeout | "rate_limited">;
241
+ * const status = r.match({
242
+ * ok: () => 200,
243
+ * errCases: (matcher) =>
244
+ * matcher
245
+ * .with(P.tag("NotFound"), () => 404) // a TaggedError, narrowed with its payload
246
+ * .with(P.tag("Conflict"), () => 409)
247
+ * .with(P.instanceOf(VendorTimeout), (e) => (e.afterMs > 10_000 ? 504 : 503))
248
+ * .with(
249
+ * P.when((v): v is "rate_limited" => v === "rate_limited"),
250
+ * () => 429,
251
+ * ),
252
+ * defect: () => 500,
253
+ * });
254
+ * ```
166
255
  */
167
256
  const P = Object.freeze({
168
257
  _: universal,
169
- tag: (value) => ({ _tag: value }),
258
+ tag: (value) => Object.freeze({ _tag: value }),
170
259
  instanceOf: (cls) => pattern((value) => value instanceof cls),
171
260
  when: (guard) => pattern(guard)
172
261
  });
@@ -328,6 +417,7 @@ var Res = class {
328
417
  try {
329
418
  const out = runMatch(f, this.error);
330
419
  if (isDefectMarker(out)) return defectRes(out.cause);
420
+ if (isThenable(out)) return asyncBranchDefect(out);
331
421
  return errRes(out);
332
422
  } catch (cause) {
333
423
  return defectRes(cause);
@@ -349,6 +439,7 @@ var Res = class {
349
439
  try {
350
440
  const out = runMatch(f, this.error);
351
441
  if (isDefectMarker(out)) return defectRes(out.cause);
442
+ if (isThenable(out)) return asyncBranchDefect(out);
352
443
  return okRes(out);
353
444
  } catch (cause) {
354
445
  return defectRes(cause);
@@ -518,7 +609,8 @@ function defectRes(cause) {
518
609
  * brand the prototype carries — so a `Result` built by **another copy** of
519
610
  * unthrown, e.g. the CJS and ESM builds loaded side by side, is still
520
611
  * recognised). A look-alike plain object (`{ tag: "Ok" }`) carries neither and
521
- * is **not** matched. An `AsyncResult` is not a `Result` and returns `false`.
612
+ * is **not** matched; nor is a forgery built on the real prototype whose `tag` or
613
+ * payload is a getter (both must be own data properties). An `AsyncResult` is not a `Result` and returns `false`.
522
614
  *
523
615
  * @returns `true` when `x` is a `Result` produced by this library.
524
616
  *
@@ -545,13 +637,42 @@ function defectRes(cause) {
545
637
  * @category Guards
546
638
  */
547
639
  function isResult(x) {
548
- if (x instanceof Res) return true;
549
640
  try {
550
- return (typeof x === "object" || typeof x === "function") && x !== null && Reflect.get(x, RESULT_BRAND) === true;
641
+ return (x instanceof Res || (typeof x === "object" || typeof x === "function") && x !== null && Reflect.get(x, RESULT_BRAND) === true) && hasOwnVariantShape(x);
551
642
  } catch {
552
643
  return false;
553
644
  }
554
645
  }
646
+ /** Each variant's payload key. @internal */
647
+ const PAYLOAD_KEY = {
648
+ Ok: "value",
649
+ Err: "error",
650
+ Defect: "cause"
651
+ };
652
+ /**
653
+ * Does `x` carry a variant's shape as own **data** properties — `tag` one of
654
+ * the three variants, plus that variant's payload key?
655
+ *
656
+ * @remarks
657
+ * A brand is not enough: the prototype (and so the brand) is reachable from any
658
+ * genuine `Result`, so `Object.create(protoOf(Ok(1)))` with a throwing `tag` or
659
+ * payload getter passed the guard and then threw — raw out of `all`, or as a
660
+ * rejection out of an `AsyncResult` that must never reject. Every genuine
661
+ * `Result`, from any copy of the library, is a frozen object literal whose `tag`
662
+ * and payload are own data properties, so reading their descriptors (which
663
+ * never runs a getter) accepts all of them and no getter-bearing forgery.
664
+ *
665
+ * @internal
666
+ */
667
+ function hasOwnVariantShape(x) {
668
+ const tag = Object.getOwnPropertyDescriptor(x, "tag");
669
+ if (tag === void 0 || !("value" in tag)) return false;
670
+ const variant = tag.value;
671
+ if (variant !== "Ok" && variant !== "Err" && variant !== "Defect") return false;
672
+ const key = PAYLOAD_KEY[variant];
673
+ const payload = Object.getOwnPropertyDescriptor(x, key);
674
+ return payload !== void 0 && "value" in payload;
675
+ }
555
676
  /**
556
677
  * Reuse a non-matching variant (an `Err` or `Defect`) as a differently-typed
557
678
  * `Result`, with no runtime work. Sound because the passed-through variant
@@ -577,9 +698,9 @@ function passThrough(self) {
577
698
  * - inside a `try` that routes the throw to a `Defect` (the boundaries in
578
699
  * `interop.ts`, where a hostile value arriving at a triage point *is* an
579
700
  * unmodeled failure and should surface as one); or
580
- * - through {@link silenceIfThenable}, for a value being **discarded**, where
581
- * there is no Defect channel to route to and the only correct answer is to
582
- * drop it without throwing.
701
+ * - to classify a value that is then **discarded** (a `Defect` minted, the
702
+ * value handed to {@link silenceIfThenable}), where the only correct answer
703
+ * is to drop it without throwing — and without starting a lazy thenable.
583
704
  *
584
705
  * Calling it bare, outside both, is a bug: the throw escapes into whatever
585
706
  * context invoked it.
@@ -590,27 +711,37 @@ function isThenable(x) {
590
711
  return (typeof x === "object" || typeof x === "function") && x !== null && typeof x.then === "function";
591
712
  }
592
713
  /**
593
- * Adopt-and-silence a thenable a combinator is about to **discard**.
714
+ * Silence a **genuine `Promise`** a combinator or boundary is about to
715
+ * **discard** — and leave every other thenable untouched.
594
716
  *
595
717
  * @remarks
596
718
  * The observers (`tap`, `tapErrCases`, `tapDefect`, `tapFailure`) throw their
597
719
  * callback's return value away, and the `Result`-returning combinators reject a
598
- * non-`Result` one. Either way, a thenable that slipped past `NotThenable` (a
720
+ * non-`Result` one. Either way, a promise that slipped past `NotThenable` (a
599
721
  * cast, a raw-JS caller) is dropped while still in flight — and if it later
600
722
  * rejects, nothing is holding it, so the rejection floats unhandled and takes
601
723
  * the process down on Node by default. Worse for an observer: its whole job is
602
724
  * to make a failure visible, and this is the one path where the failure is
603
- * invisible.
725
+ * invisible. Attaching a no-op rejection handler costs nothing and changes no
726
+ * outcome.
604
727
  *
605
- * Adopting it costs one microtask and makes the rejection a no-op. The
606
- * boundaries already do exactly this for a thenable `qualify` and a thenable
607
- * `fn` (see `interop.ts`); this is the same net on the combinator side.
728
+ * Only a `Promise` **instance** is touched. A promise is already running, so
729
+ * handling its rejection starts nothing; a non-`Promise` thenable may be
730
+ * **lazy** — a `PrismaPromise`, a query builder — whose work begins only when
731
+ * `then` is called. Adopting one (`Promise.resolve(x)` calls `x.then`) would
732
+ * *run* the effect the caller is being told was refused: `fromSafeThrowable(()
733
+ * => prisma.user.deleteMany())` returned a `Defect` and deleted the rows
734
+ * anyway. A lazy thenable that is never started cannot reject, so there is
735
+ * nothing to silence. Callers still *classify* any thenable (a `Defect` where
736
+ * the spec says so) via {@link isThenable}; this only decides what to adopt.
737
+ *
738
+ * Total: a hostile `then` getter or `Symbol.hasInstance` path is swallowed.
608
739
  *
609
740
  * @internal
610
741
  */
611
742
  function silenceIfThenable(value) {
612
743
  try {
613
- if (isThenable(value)) Promise.resolve(value).then(void 0, () => void 0);
744
+ if (value instanceof Promise) value.then(void 0, () => void 0);
614
745
  } catch {}
615
746
  }
616
747
  /**
@@ -628,6 +759,19 @@ function nonResultCallbackDefect(returned) {
628
759
  return defectRes(/* @__PURE__ */ new TypeError("unthrown: a combinator callback returned a non-Result value"));
629
760
  }
630
761
  /**
762
+ * The Defect minted when a non-awaiting error transformer (`mapErrCases` /
763
+ * `recoverErrCases`) gets a thenable branch output past the compile-time ban
764
+ * (a cast, an untyped caller): never `Err(<Promise>)` / `Ok(<Promise>)` — a
765
+ * Promise in the channel is un-triaged — and a genuine Promise is silenced so
766
+ * its rejection cannot float. The sibling of the aggregates' async-`merge` net.
767
+ *
768
+ * @internal
769
+ */
770
+ function asyncBranchDefect(out) {
771
+ silenceIfThenable(out);
772
+ return defectRes(/* @__PURE__ */ new TypeError("unthrown: mapErrCases/recoverErrCases branches must be SYNCHRONOUS, but one returned a thenable — lift async work with fromPromise and use flatMapErrCases"));
773
+ }
774
+ /**
631
775
  * Drive an error-combinator callback: build `match(error)`, hand it (plus the
632
776
  * injected `defect`) to the callback, and `.run()` the returned exhaustive
633
777
  * builder to its output. `.run()` executes `.exhaustive()` — type-forced
@@ -676,10 +820,23 @@ function observerThrowToDefect(thrown, original) {
676
820
  * @internal
677
821
  */
678
822
  function scopeOf(value) {
679
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("bind/let requires an object scope — start a do-chain with Do()");
823
+ if (typeof value !== "object" || value === null || !isPlainScope(value)) throw new TypeError("bind/let requires a plain object scope — start a do-chain with Do()");
680
824
  return value;
681
825
  }
682
826
  /**
827
+ * A *plain* object: its prototype is `null` or an `Object.prototype` (any
828
+ * realm's — recognised by having no prototype of its own). The spread that
829
+ * merges a `bind`/`let` key copies only own enumerable data, so a class
830
+ * instance's getters and prototype methods — and an array's identity — would
831
+ * silently vanish while the type still claimed them.
832
+ *
833
+ * @internal
834
+ */
835
+ function isPlainScope(value) {
836
+ const proto = Object.getPrototypeOf(value);
837
+ return proto === null || Object.getPrototypeOf(proto) === null;
838
+ }
839
+ /**
683
840
  * The sole runtime implementation of {@link AsyncResult}: wraps a
684
841
  * `Promise<Result>` constructed never to reject. Operates on the public `Result`
685
842
  * union (via `tag`), never on `Res` internals. Never re-exported from `index.ts`.
@@ -769,7 +926,7 @@ var AsyncRes = class AsyncRes {
769
926
  ensure(predicate, onFail) {
770
927
  return this.#lift((r) => r.ensure(predicate, onFail));
771
928
  }
772
- mapErrCases(f) {
929
+ mapErrCases(f, ..._guard) {
773
930
  return this.#lift((r) => r.mapErrCases(f));
774
931
  }
775
932
  flatMapErrCases(f) {
@@ -786,7 +943,7 @@ var AsyncRes = class AsyncRes {
786
943
  }
787
944
  }));
788
945
  }
789
- recoverErrCases(f) {
946
+ recoverErrCases(f, ..._guard) {
790
947
  return this.#lift((r) => r.recoverErrCases(f));
791
948
  }
792
949
  tapErrCases(f) {
@@ -1124,12 +1281,17 @@ function fromNullable(value, onAbsent) {
1124
1281
  function fromThrowable(fn, qualify) {
1125
1282
  const triage = qualify;
1126
1283
  return (...args) => {
1284
+ let value;
1127
1285
  try {
1128
- const value = fn(...args);
1129
- return isThenable(value) ? thenableReturnDefect(value) : Ok(value);
1286
+ value = fn(...args);
1130
1287
  } catch (cause) {
1131
1288
  return qualifyToResult(cause, triage);
1132
1289
  }
1290
+ try {
1291
+ return isThenable(value) ? thenableReturnDefect(value, SYNC_FN_THENABLE) : Ok(value);
1292
+ } catch (cause) {
1293
+ return defectRes(cause);
1294
+ }
1133
1295
  };
1134
1296
  }
1135
1297
  /**
@@ -1169,7 +1331,7 @@ function fromSafeThrowable(fn) {
1169
1331
  return (...args) => {
1170
1332
  try {
1171
1333
  const value = fn(...args);
1172
- return isThenable(value) ? thenableReturnDefect(value) : Ok(value);
1334
+ return isThenable(value) ? thenableReturnDefect(value, SYNC_FN_THENABLE) : Ok(value);
1173
1335
  } catch (cause) {
1174
1336
  return defectRes(cause);
1175
1337
  }
@@ -1311,7 +1473,7 @@ function fromExecutor(executor) {
1311
1473
  };
1312
1474
  try {
1313
1475
  const returned = executor(settle, defect);
1314
- if (isThenable(returned)) Promise.resolve(returned).then(void 0, (cause) => settle(defect(cause)));
1476
+ if (returned instanceof Promise) returned.then(void 0, (cause) => settle(defect(cause)));
1315
1477
  } catch (cause) {
1316
1478
  settle(defect(cause));
1317
1479
  }
@@ -1322,7 +1484,7 @@ function qualifyToResult(cause, qualify) {
1322
1484
  const q = qualify(cause, defect);
1323
1485
  if (isDefectMarker(q)) return defectRes(q.cause);
1324
1486
  if (isThenable(q)) {
1325
- Promise.resolve(q).then(void 0, () => void 0);
1487
+ silenceIfThenable(q);
1326
1488
  return defectRes(/* @__PURE__ */ new TypeError("unthrown: qualify must be synchronous — it returned a thenable; triage the cause without awaiting"));
1327
1489
  }
1328
1490
  return errRes(q);
@@ -1331,30 +1493,78 @@ function qualifyToResult(cause, qualify) {
1331
1493
  }
1332
1494
  }
1333
1495
  /**
1334
- * The Defect minted when a **synchronous** boundary's `fn` returns a thenable —
1335
- * i.e. an `async` function was handed to {@link fromThrowable} /
1336
- * {@link fromSafeThrowable}.
1496
+ * The message for {@link thenableReturnDefect} at a **synchronous boundary** —
1497
+ * an `async` function handed to {@link fromThrowable} / {@link fromSafeThrowable}.
1498
+ *
1499
+ * @internal
1500
+ */
1501
+ const SYNC_FN_THENABLE = "unthrown: fromThrowable/fromSafeThrowable wrap a SYNCHRONOUS function, but `fn` returned a thenable — its rejection would escape qualification. Use fromPromise/fromSafePromise instead.";
1502
+ /**
1503
+ * The message for {@link thenableReturnDefect} in an **accumulating aggregate** —
1504
+ * an `async` `merge` handed to {@link validateAll} and friends.
1505
+ *
1506
+ * @internal
1507
+ */
1508
+ const MERGE_THENABLE = "unthrown: an accumulating aggregate's `merge` must be SYNCHRONOUS, but it returned a thenable — its rejection would escape qualification.";
1509
+ /**
1510
+ * The Defect minted where a callback that must be **synchronous** returned a
1511
+ * thenable: a `fn` handed to {@link fromThrowable} / {@link fromSafeThrowable},
1512
+ * or a `merge` handed to an accumulating aggregate.
1337
1513
  *
1338
1514
  * @remarks
1339
1515
  * This is the sibling of the thenable-`qualify` net in {@link qualifyToResult},
1340
1516
  * and it closes a strictly worse hole. A synchronous boundary only ever sees a
1341
1517
  * synchronous `throw`, so an async `fn`'s rejection never reaches `qualify` at
1342
1518
  * all: it would sit inside `Ok(<Promise>)`, un-triaged, and then float as an
1343
- * unhandled rejection — which terminates the process on Node by default.
1519
+ * unhandled rejection — which terminates the process on Node by default. An
1520
+ * async `merge` is the same hole one channel over: `Err(<Promise>)`.
1521
+ *
1522
+ * The `fn` case cannot be banned at compile time without collateral damage:
1523
+ * `T & NotThenable<T>` on `fn`'s return makes a **generic** function
1524
+ * unassignable, so `fromSafeThrowable(structuredClone)` stops compiling and `T`
1525
+ * collapses to `unknown`. (The phantom rest-tuple guard `fromPromise` uses fares
1526
+ * worse.) `merge` *is* `NotThenable`-constrained, but a cast or an untyped
1527
+ * caller still reaches here. Either way the runtime answer is the same, and it
1528
+ * costs nothing: a Defect, plus a no-op rejection handler so an orphaned Promise
1529
+ * cannot float.
1344
1530
  *
1345
- * Unlike the combinator callbacks, this cannot be banned at compile time
1346
- * without collateral damage: `T & NotThenable<T>` on `fn`'s return makes a
1347
- * **generic** function unassignable, so `fromSafeThrowable(structuredClone)`
1348
- * stops compiling and `T` collapses to `unknown`. (The phantom rest-tuple guard
1349
- * `fromPromise` uses fares worse.) So the ban is enforced here, at runtime,
1350
- * where it costs nothing: a Defect, plus adopt-and-silence so the orphaned
1351
- * rejection cannot float.
1531
+ * @internal
1532
+ */
1533
+ function thenableReturnDefect(value, message) {
1534
+ silenceIfThenable(value);
1535
+ return defectRes(new TypeError(message));
1536
+ }
1537
+ /**
1538
+ * A record's own **enumerable** keys — strings and symbols — with their values.
1539
+ *
1540
+ * @remarks
1541
+ * `Object.keys` / `Object.values` skip symbol keys, so a symbol-keyed `Err`
1542
+ * silently vanished from the fold while the types (`keyof R` includes it)
1543
+ * promised otherwise. `Reflect.ownKeys` filtered to the enumerable ones is
1544
+ * `Object.keys` plus symbols, in the same order (strings first, then symbols).
1545
+ * May throw on an out-of-contract container (`null`, a throwing getter) — every
1546
+ * caller routes that to a `Defect`.
1547
+ *
1548
+ * @internal
1549
+ */
1550
+ function ownEntries(record) {
1551
+ const keys = Reflect.ownKeys(record).filter((key) => Object.prototype.propertyIsEnumerable.call(record, key));
1552
+ return [keys, keys.map((key) => record[key])];
1553
+ }
1554
+ /**
1555
+ * Build an async aggregate's settled promise, turning a synchronous throw while
1556
+ * reading an out-of-contract container (`allAsync(undefined)`, a throwing
1557
+ * getter) into a `Defect` — the returned `AsyncResult` still never rejects and
1558
+ * the call never throws.
1352
1559
  *
1353
1560
  * @internal
1354
1561
  */
1355
- function thenableReturnDefect(value) {
1356
- Promise.resolve(value).then(void 0, () => void 0);
1357
- return defectRes(/* @__PURE__ */ new TypeError("unthrown: fromThrowable/fromSafeThrowable wrap a SYNCHRONOUS function, but `fn` returned a thenable — its rejection would escape qualification. Use fromPromise/fromSafePromise instead."));
1562
+ function settleOrDefect(build) {
1563
+ try {
1564
+ return new AsyncRes(build());
1565
+ } catch (cause) {
1566
+ return new AsyncRes(Promise.resolve(defectRes(cause)));
1567
+ }
1358
1568
  }
1359
1569
  /**
1360
1570
  * Fold an array of settled `Result`s: first `Err` wins, any `Defect` dominates,
@@ -1366,22 +1576,46 @@ function thenableReturnDefect(value) {
1366
1576
  function nonResultDefect() {
1367
1577
  return defectRes(/* @__PURE__ */ new TypeError("unthrown: aggregate received a non-Result element"));
1368
1578
  }
1369
- function foldArray(results) {
1370
- let firstErr;
1371
- let firstDefect;
1372
- const values = [];
1373
- for (const r of results) {
1374
- if (!isResult(r)) {
1375
- firstDefect ??= nonResultDefect();
1376
- break;
1579
+ /**
1580
+ * Resolve every input concurrently (order preserved), adopting each one
1581
+ * defensively: a cast/untyped rejecting thenable becomes a `Defect` rather than
1582
+ * rejecting the internal promise, so the "an `AsyncResult`'s internal promise
1583
+ * never rejects" invariant holds even for out-of-contract input.
1584
+ *
1585
+ * @internal
1586
+ */
1587
+ function settleAll(results) {
1588
+ return Promise.all(results.map((r) => Promise.resolve(r).then((x) => x, (cause) => defectRes(cause))));
1589
+ }
1590
+ function foldArray(results, merge) {
1591
+ try {
1592
+ let firstErr;
1593
+ let firstDefect;
1594
+ const values = [];
1595
+ const errors = [];
1596
+ for (const [i, r] of results.entries()) {
1597
+ if (!isResult(r)) {
1598
+ firstDefect ??= nonResultDefect();
1599
+ break;
1600
+ }
1601
+ if (r.tag === "Defect") {
1602
+ firstDefect ??= r;
1603
+ break;
1604
+ } else if (r.tag === "Err") {
1605
+ if (merge) errors.push([i, r.error]);
1606
+ else firstErr ??= r;
1607
+ } else values.push(r.value);
1377
1608
  }
1378
- if (r.tag === "Defect") {
1379
- firstDefect ??= r;
1380
- break;
1381
- } else if (r.tag === "Err") firstErr ??= r;
1382
- else values.push(r.value);
1609
+ if (firstDefect) return firstDefect;
1610
+ if (merge && errors.length > 0) {
1611
+ const merged = merge(errors);
1612
+ if (isThenable(merged)) return thenableReturnDefect(merged, MERGE_THENABLE);
1613
+ return Err(merged);
1614
+ }
1615
+ return firstErr ?? Ok(values);
1616
+ } catch (cause) {
1617
+ return defectRes(cause);
1383
1618
  }
1384
- return firstDefect ?? firstErr ?? Ok(values);
1385
1619
  }
1386
1620
  /**
1387
1621
  * Fold a record of settled `Result`s with the same rules, else `Ok` of the
@@ -1399,9 +1633,23 @@ function foldArray(results) {
1399
1633
  *
1400
1634
  * @internal
1401
1635
  */
1402
- function foldRecord(results) {
1403
- const keys = Object.keys(results);
1404
- return foldArray(Object.values(results)).map((values) => Object.fromEntries(keys.map((key, i) => [key, values[i]])));
1636
+ function foldRecord(results, merge) {
1637
+ let keys;
1638
+ let values;
1639
+ try {
1640
+ [keys, values] = ownEntries(results);
1641
+ } catch (cause) {
1642
+ return defectRes(cause);
1643
+ }
1644
+ return foldArray(values, merge && ((errors) => merge(nameErrors(errors, keys)))).map((values) => Object.fromEntries(keys.map((key, i) => [key, values[i]])));
1645
+ }
1646
+ /** Drop the accumulated indices — the positional forms merge errors alone. @internal */
1647
+ function stripIndices(errors) {
1648
+ return errors.map(([, e]) => e);
1649
+ }
1650
+ /** Pair each accumulated index back onto its key. @internal */
1651
+ function nameErrors(errors, keys) {
1652
+ return errors.map(([i, e]) => [keys[i], e]);
1405
1653
  }
1406
1654
  /**
1407
1655
  * Collect a tuple/array of {@link Result}s into a single `Result` of all their
@@ -1413,7 +1661,8 @@ function foldRecord(results) {
1413
1661
  * `Err`. A **fixed tuple** keeps its positional types — `all([Ok(1), Ok("a")])`
1414
1662
  * is `Result<[number, string], …>` — while a **dynamic array** `Result<T, E>[]`
1415
1663
  * collapses to `Result<T[], E>` with no cast. For a **record** keyed by name,
1416
- * use {@link allFromDict}.
1664
+ * use {@link allFromDict}. To report **every** `Err` instead of only the first,
1665
+ * use {@link validateAll}.
1417
1666
  *
1418
1667
  * @category Aggregate
1419
1668
  *
@@ -1436,7 +1685,9 @@ function all(results) {
1436
1685
  *
1437
1686
  * @remarks
1438
1687
  * Same folding rules as {@link all}: first `Err` short-circuits, any `Defect`
1439
- * dominates. This is **not** error accumulation.
1688
+ * dominates. This is **not** error accumulation — for that, reach for
1689
+ * {@link validateAllFromDict}, which accumulates every `Err` and folds them into
1690
+ * one modeled error.
1440
1691
  *
1441
1692
  * @category Aggregate
1442
1693
  *
@@ -1459,7 +1710,8 @@ function allFromDict(results) {
1459
1710
  * The inputs are resolved **concurrently** (order preserved); the resolved
1460
1711
  * `Result`s are then folded with the same rules as {@link all} — first `Err`
1461
1712
  * short-circuits, any `Defect` dominates. As ever, the returned `AsyncResult`'s
1462
- * internal promise never rejects. For a **record**, use {@link allFromDictAsync}.
1713
+ * internal promise never rejects. For a **record**, use {@link allFromDictAsync};
1714
+ * to report **every** `Err`, use {@link validateAllAsync}.
1463
1715
  *
1464
1716
  * @category Aggregate
1465
1717
  *
@@ -1475,7 +1727,7 @@ function allFromDict(results) {
1475
1727
  * ```
1476
1728
  */
1477
1729
  function allAsync(results) {
1478
- return new AsyncRes(Promise.all(results.map((r) => Promise.resolve(r).then((x) => x, (cause) => defectRes(cause)))).then((resolved) => foldArray(resolved)));
1730
+ return settleOrDefect(() => settleAll(results).then((resolved) => foldArray(resolved)));
1479
1731
  }
1480
1732
  /**
1481
1733
  * The asynchronous counterpart of {@link allFromDict}: combine a record of
@@ -1483,7 +1735,8 @@ function allAsync(results) {
1483
1735
  *
1484
1736
  * @remarks
1485
1737
  * Resolved concurrently (order preserved), folded with the {@link all} rules,
1486
- * and the internal promise never rejects.
1738
+ * and the internal promise never rejects. To report **every** `Err`, use
1739
+ * {@link validateAllFromDictAsync}.
1487
1740
  *
1488
1741
  * @category Aggregate
1489
1742
  *
@@ -1499,8 +1752,169 @@ function allAsync(results) {
1499
1752
  * ```
1500
1753
  */
1501
1754
  function allFromDictAsync(results) {
1502
- const keys = Object.keys(results);
1503
- return new AsyncRes(Promise.all(Object.values(results).map((ar) => Promise.resolve(ar).then((x) => x, (cause) => defectRes(cause)))).then((resolved) => foldArray(resolved).map((values) => Object.fromEntries(keys.map((key, i) => [key, values[i]])))));
1755
+ return settleOrDefect(() => {
1756
+ const [keys, values] = ownEntries(results);
1757
+ return settleAll(values).then((resolved) => foldRecord(Object.fromEntries(keys.map((key, i) => [key, resolved[i]]))));
1758
+ });
1759
+ }
1760
+ /**
1761
+ * Collect a tuple/array of {@link Result}s, **accumulating every** `Err` and
1762
+ * merging them into a single modeled error — the accumulating counterpart of
1763
+ * {@link all}.
1764
+ *
1765
+ * @remarks
1766
+ * Same success channel as {@link all}: a **fixed tuple** keeps its positional
1767
+ * types, a **dynamic array** collapses to `Result<T[], E2>`. The difference is
1768
+ * the error channel — instead of the first `Err` winning, every `Err` is
1769
+ * collected in input order and handed to `merge`, whose return becomes the
1770
+ * modeled error.
1771
+ *
1772
+ * `merge` receives a **non-empty** list, so it is total: it is called only when
1773
+ * at least one `Err` was collected. It is **not** called when every element is
1774
+ * `Ok`, nor when a `Defect` is present.
1775
+ *
1776
+ * Any `Defect` still **dominates** — it wins over the accumulated errors, which
1777
+ * are discarded and never reach `merge`. A defect means something in this batch
1778
+ * failed in a way nobody modeled, so the violations computed alongside it are
1779
+ * not trustworthy. An out-of-contract non-`Result` element becomes a
1780
+ * `TypeError`-caused `Defect` the same way, and a throw inside `merge` becomes
1781
+ * a `Defect` too.
1782
+ *
1783
+ * `merge` must be **synchronous** — an `async` one is a compile error
1784
+ * ({@link NotThenable}), since a `Promise` in `E` is an unqualified rejection.
1785
+ *
1786
+ * For **schema-shaped** input (a request body, a form), reach for
1787
+ * `@unthrown/standard-schema`'s `fromSchema` instead — a validator already
1788
+ * hands you every issue as the modeled error. `validateAll` is for independent
1789
+ * checks you wrote yourself. For a **record** keyed by name, use
1790
+ * {@link validateAllFromDict}.
1791
+ *
1792
+ * @typeParam Rs - the tuple/array of input `Result` types.
1793
+ * @typeParam E2 - the merged error type.
1794
+ * @param results - the results to collect.
1795
+ * @param merge - folds the collected errors into one modeled error.
1796
+ *
1797
+ * @category Aggregate
1798
+ *
1799
+ * @example
1800
+ * ```ts
1801
+ * import { validateAll, Ok, Err } from "unthrown";
1802
+ *
1803
+ * // every Err is collected, not just the first
1804
+ * validateAll([Ok(1), Err("stock"), Err("credit")], (errors) => errors.join(" and "));
1805
+ * // => Err("stock and credit")
1806
+ *
1807
+ * // all-Ok keeps the positional tuple; `merge` never runs
1808
+ * validateAll([Ok(1), Ok("a")], (errors) => errors.join());
1809
+ * // => Ok([1, "a"]) typed Result<[number, string], string>
1810
+ * ```
1811
+ */
1812
+ function validateAll(results, merge) {
1813
+ return foldArray(results, (errors) => merge(stripIndices(errors)));
1814
+ }
1815
+ /**
1816
+ * Collect a **record** of {@link Result}s, accumulating every `Err` — the
1817
+ * accumulating counterpart of {@link allFromDict}, and the named counterpart of
1818
+ * {@link validateAll}.
1819
+ *
1820
+ * @remarks
1821
+ * `merge` receives a non-empty list of **`[key, error]` entries**, correlated
1822
+ * per key: `{ a: Result<A, E1>; b: Result<B, E2> }` yields
1823
+ * `["a", E1] | ["b", E2]`, so a `switch` on the key narrows the error and an
1824
+ * impossible pairing does not typecheck. That is what keeps two checks sharing
1825
+ * one error type distinguishable. Entries come in key order — `Object.keys`
1826
+ * order, then enumerable symbol keys (a symbol key is folded like any other).
1827
+ *
1828
+ * Every other rule matches {@link validateAll}: any `Defect` dominates and
1829
+ * discards the accumulated errors, a throw in `merge` becomes a `Defect`, and
1830
+ * `merge` must be synchronous.
1831
+ *
1832
+ * @typeParam R - the record of input `Result` types.
1833
+ * @typeParam E2 - the merged error type.
1834
+ * @param results - the results to collect, keyed by name.
1835
+ * @param merge - folds the collected `[key, error]` entries into one error.
1836
+ *
1837
+ * @category Aggregate
1838
+ *
1839
+ * @example
1840
+ * ```ts
1841
+ * import { validateAllFromDict, Ok, Err } from "unthrown";
1842
+ *
1843
+ * validateAllFromDict(
1844
+ * { vatRate: Err("out of range"), currency: Ok("EUR"), dueDate: Err("past") },
1845
+ * (entries) => entries.map(([key, error]) => `${key}: ${error}`).join("; "),
1846
+ * );
1847
+ * // => Err("vatRate: out of range; dueDate: past")
1848
+ * ```
1849
+ */
1850
+ function validateAllFromDict(results, merge) {
1851
+ return foldRecord(results, (entries) => merge(entries));
1852
+ }
1853
+ /**
1854
+ * The asynchronous counterpart of {@link validateAll}: collect a tuple/array of
1855
+ * {@link AsyncResult}s, accumulating every `Err` into one merged error.
1856
+ *
1857
+ * @remarks
1858
+ * Every {@link validateAll} rule holds, with the inputs resolved
1859
+ * **concurrently** (order preserved) — as with {@link allAsync}, no work is
1860
+ * short-circuited either way; the fail-fast/accumulating split is purely which
1861
+ * errors get reported. The internal promise never rejects: an out-of-contract
1862
+ * rejecting thenable becomes a dominating `Defect`. `merge` stays synchronous
1863
+ * here too — this is exactly where its rejection would land unqualified in `E`.
1864
+ * For a **record**, use {@link validateAllFromDictAsync}.
1865
+ *
1866
+ * @typeParam Rs - the tuple/array of input `AsyncResult` types.
1867
+ * @typeParam E2 - the merged error type.
1868
+ * @param results - the async results to collect.
1869
+ * @param merge - folds the collected errors into one modeled error.
1870
+ *
1871
+ * @category Aggregate
1872
+ *
1873
+ * @example
1874
+ * ```ts
1875
+ * import { validateAllAsync, OkAsync, ErrAsync } from "unthrown";
1876
+ *
1877
+ * const checked = validateAllAsync(
1878
+ * [OkAsync(1), ErrAsync("stock"), ErrAsync("credit")],
1879
+ * (errors) => errors.join(" and "),
1880
+ * );
1881
+ * // (await checked) => Err("stock and credit")
1882
+ * ```
1883
+ */
1884
+ function validateAllAsync(results, merge) {
1885
+ return settleOrDefect(() => settleAll(results).then((resolved) => foldArray(resolved, (errors) => merge(stripIndices(errors)))));
1886
+ }
1887
+ /**
1888
+ * The asynchronous counterpart of {@link validateAllFromDict}: collect a record
1889
+ * of {@link AsyncResult}s, accumulating every `Err` into one merged error.
1890
+ *
1891
+ * @remarks
1892
+ * The {@link validateAllFromDict} rules, over inputs resolved concurrently as
1893
+ * in {@link validateAllAsync}.
1894
+ *
1895
+ * @typeParam R - the record of input `AsyncResult` types.
1896
+ * @typeParam E2 - the merged error type.
1897
+ * @param results - the async results to collect, keyed by name.
1898
+ * @param merge - folds the collected `[key, error]` entries into one error.
1899
+ *
1900
+ * @category Aggregate
1901
+ *
1902
+ * @example
1903
+ * ```ts
1904
+ * import { validateAllFromDictAsync, OkAsync, ErrAsync } from "unthrown";
1905
+ *
1906
+ * const checked = validateAllFromDictAsync(
1907
+ * { stock: ErrAsync("none left"), credit: OkAsync(500) },
1908
+ * (entries) => entries.map(([key, error]) => `${key}: ${error}`).join("; "),
1909
+ * );
1910
+ * // (await checked) => Err("stock: none left")
1911
+ * ```
1912
+ */
1913
+ function validateAllFromDictAsync(results, merge) {
1914
+ return settleOrDefect(() => {
1915
+ const [keys, values] = ownEntries(results);
1916
+ return settleAll(values).then((resolved) => foldRecord(Object.fromEntries(keys.map((key, i) => [key, resolved[i]])), (entries) => merge(entries)));
1917
+ });
1504
1918
  }
1505
1919
  //#endregion
1506
1920
  //#region src/facade.ts
@@ -1509,7 +1923,8 @@ function allFromDictAsync(results) {
1509
1923
  * single, discoverable namespace: {@link Result.Ok}, {@link Result.Err},
1510
1924
  * {@link Result.Do}, {@link Result.fromNullable}, {@link Result.fromThrowable},
1511
1925
  * {@link Result.fromSafeThrowable}, {@link Result.all},
1512
- * {@link Result.allFromDict}, {@link Result.isOk}, {@link Result.isErr},
1926
+ * {@link Result.allFromDict}, {@link Result.validateAll},
1927
+ * {@link Result.validateAllFromDict}, {@link Result.isOk}, {@link Result.isErr},
1513
1928
  * {@link Result.isDefect}, {@link Result.isResult}.
1514
1929
  *
1515
1930
  * @remarks
@@ -1539,6 +1954,8 @@ const Result = {
1539
1954
  fromSafeThrowable,
1540
1955
  all,
1541
1956
  allFromDict,
1957
+ validateAll,
1958
+ validateAllFromDict,
1542
1959
  isOk,
1543
1960
  isErr,
1544
1961
  isDefect,
@@ -1549,7 +1966,8 @@ const Result = {
1549
1966
  * the matching namespace: {@link AsyncResult.Ok}, {@link AsyncResult.Err},
1550
1967
  * {@link AsyncResult.Do}, {@link AsyncResult.fromExecutor},
1551
1968
  * {@link AsyncResult.fromPromise}, {@link AsyncResult.fromSafePromise},
1552
- * {@link AsyncResult.all}, {@link AsyncResult.allFromDict}.
1969
+ * {@link AsyncResult.all}, {@link AsyncResult.allFromDict},
1970
+ * {@link AsyncResult.validateAll}, {@link AsyncResult.validateAllFromDict}.
1553
1971
  *
1554
1972
  * @remarks
1555
1973
  * The async sibling of {@link Result}. Statics are grouped by what they
@@ -1560,7 +1978,8 @@ const Result = {
1560
1978
  * functions carry (`AsyncResult.Ok` is `OkAsync`; `AsyncResult.Err` is
1561
1979
  * `ErrAsync`; `AsyncResult.Do` is `DoAsync`; `AsyncResult.all` is `allAsync`;
1562
1980
  * `AsyncResult.allFromDict` is
1563
- * `allFromDictAsync`). Like {@link Result}, the free functions remain the
1981
+ * `allFromDictAsync`; `AsyncResult.validateAll` is `validateAllAsync`). Like
1982
+ * {@link Result}, the free functions remain the
1564
1983
  * primary, tree-shakeable API; the value `AsyncResult` and the type
1565
1984
  * {@link AsyncResult} share one name.
1566
1985
  *
@@ -1584,7 +2003,9 @@ const AsyncResult = {
1584
2003
  fromPromise,
1585
2004
  fromSafePromise,
1586
2005
  all: allAsync,
1587
- allFromDict: allFromDictAsync
2006
+ allFromDict: allFromDictAsync,
2007
+ validateAll: validateAllAsync,
2008
+ validateAllFromDict: validateAllFromDictAsync
1588
2009
  };
1589
2010
  //#endregion
1590
2011
  //#region src/tagged.ts
@@ -1704,3 +2125,7 @@ exports.isErr = isErr;
1704
2125
  exports.isOk = isOk;
1705
2126
  exports.isResult = isResult;
1706
2127
  exports.match = match;
2128
+ exports.validateAll = validateAll;
2129
+ exports.validateAllAsync = validateAllAsync;
2130
+ exports.validateAllFromDict = validateAllFromDict;
2131
+ exports.validateAllFromDictAsync = validateAllFromDictAsync;