unthrown 4.3.0 → 5.0.0-beta.1

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
@@ -1,4 +1,35 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let ts_pattern = require("ts-pattern");
3
+ //#region src/defect.ts
4
+ const DEFECT = Symbol("unthrown/Defect");
5
+ /**
6
+ * Wrap a cause as a `Defect` marker — the value returned from a `qualify`
7
+ * function when a failure is **not** a modeled domain error. The boundary
8
+ * (`fromPromise` / `fromThrowable`) passes this in as `qualify`'s second
9
+ * argument, so domain code never imports it.
10
+ *
11
+ * @param cause - the original thrown/rejected value.
12
+ * @returns an opaque Defect marker carrying `cause`.
13
+ *
14
+ * @internal
15
+ */
16
+ function defect(cause) {
17
+ return Object.freeze({
18
+ [DEFECT]: true,
19
+ cause
20
+ });
21
+ }
22
+ /**
23
+ * Internal guard for the qualify-time marker. Distinct from the public
24
+ * {@link isDefect} state guard — this one narrows the `E | Defect` union a
25
+ * `qualify` function returns, not a `Result`.
26
+ *
27
+ * @internal
28
+ */
29
+ function isDefectMarker(x) {
30
+ return typeof x === "object" && x !== null && x[DEFECT] === true;
31
+ }
32
+ //#endregion
2
33
  //#region src/core.ts
3
34
  /**
4
35
  * Thrown by a {@link Result}'s `get` / `getErr` when the assertion is
@@ -6,12 +37,12 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
6
37
  * `Ok`.
7
38
  *
8
39
  * @remarks
9
- * The offending value is exposed two ways: the typed {@link UnwrapError.error}
40
+ * The offending value is exposed two ways: the typed {@link GetError.error}
10
41
  * property for programmatic access, and the standard `Error.cause` for the
11
42
  * runtime and devtools to chain — when `E` is an `Error` (e.g. a `TaggedError`)
12
43
  * its original stack is printed under "caused by".
13
44
  *
14
- * A `Defect` is never wrapped in an `UnwrapError`: its original cause is
45
+ * A `Defect` is never wrapped in a `GetError`: its original cause is
15
46
  * re-thrown (with its original stack) instead.
16
47
  *
17
48
  * `get()` and `getErr()` are type-gated (`this: Result<T, never>` /
@@ -19,19 +50,19 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
19
50
  * unreachable through well-typed code — it remains only as a defensive guard
20
51
  * against unsound runtime misuse (e.g. an `as` cast past the gate).
21
52
  *
22
- * @typeParam E - the type of the {@link UnwrapError.error} it carries.
53
+ * @typeParam E - the type of the {@link GetError.error} it carries.
23
54
  *
24
55
  * @category Errors
25
56
  */
26
- var UnwrapError = class extends Error {
57
+ var GetError = class extends Error {
27
58
  /**
28
59
  * The offending value: the `Err` error for `get()`, or the `Ok` value for
29
60
  * `getErr()`.
30
61
  */
31
62
  error;
32
63
  constructor(error) {
33
- super("unthrown: called unwrap on a non-matching Result", { cause: error });
34
- this.name = "UnwrapError";
64
+ super("unthrown: get() / getErr() called on a non-matching Result variant", { cause: error });
65
+ this.name = "GetError";
35
66
  this.error = error;
36
67
  Object.setPrototypeOf(this, new.target.prototype);
37
68
  }
@@ -55,7 +86,8 @@ var Res = class {
55
86
  flatMap(f) {
56
87
  if (this.tag !== "Ok") return passThrough(this);
57
88
  try {
58
- return f(this.value);
89
+ const r = f(this.value);
90
+ return isResult(r) ? r : nonResultCallbackDefect();
59
91
  } catch (cause) {
60
92
  return defectRes(cause);
61
93
  }
@@ -73,6 +105,7 @@ var Res = class {
73
105
  if (this.tag !== "Ok") return this;
74
106
  try {
75
107
  const r = f(this.value);
108
+ if (!isResult(r)) return nonResultCallbackDefect();
76
109
  return r.tag === "Ok" ? this : passThrough(r);
77
110
  } catch (cause) {
78
111
  return defectRes(cause);
@@ -82,6 +115,7 @@ var Res = class {
82
115
  if (this.tag !== "Ok") return passThrough(this);
83
116
  try {
84
117
  const r = f(this.value);
118
+ if (!isResult(r)) return nonResultCallbackDefect();
85
119
  if (r.tag !== "Ok") return passThrough(r);
86
120
  return okRes({
87
121
  ...scopeOf(this.value),
@@ -110,10 +144,20 @@ var Res = class {
110
144
  if (this.tag !== "Ok") return passThrough(this);
111
145
  return okRes(void 0);
112
146
  }
147
+ ensure(predicate, onFail) {
148
+ if (this.tag !== "Ok") return passThrough(this);
149
+ try {
150
+ return predicate(this.value) ? this : errRes(onFail(this.value));
151
+ } catch (cause) {
152
+ return defectRes(cause);
153
+ }
154
+ }
113
155
  mapErr(f) {
114
156
  if (this.tag !== "Err") return passThrough(this);
115
157
  try {
116
- return errRes(f(this.error));
158
+ const out = runMatch(f, this.error);
159
+ if (isDefectMarker(out)) return defectRes(out.cause);
160
+ return errRes(out);
117
161
  } catch (cause) {
118
162
  return defectRes(cause);
119
163
  }
@@ -121,31 +165,28 @@ var Res = class {
121
165
  flatMapErr(f) {
122
166
  if (this.tag !== "Err") return passThrough(this);
123
167
  try {
124
- return f(this.error);
168
+ const out = runMatch(f, this.error);
169
+ if (isDefectMarker(out)) return defectRes(out.cause);
170
+ if (!isResult(out)) return nonResultCallbackDefect();
171
+ return out;
125
172
  } catch (cause) {
126
173
  return defectRes(cause);
127
174
  }
128
175
  }
129
- /** @deprecated Use {@link Res.flatMapErr}. */
130
- orElse(f) {
131
- return this.flatMapErr(f);
132
- }
133
176
  recoverErr(f) {
134
177
  if (this.tag !== "Err") return passThrough(this);
135
178
  try {
136
- return okRes(f(this.error));
179
+ const out = runMatch(f, this.error);
180
+ if (isDefectMarker(out)) return defectRes(out.cause);
181
+ return okRes(out);
137
182
  } catch (cause) {
138
183
  return defectRes(cause);
139
184
  }
140
185
  }
141
- /** @deprecated Use {@link Res.recoverErr}. */
142
- recover(f) {
143
- return this.recoverErr(f);
144
- }
145
186
  tapErr(f) {
146
187
  if (this.tag !== "Err") return this;
147
188
  try {
148
- f(this.error);
189
+ runMatch(f, this.error);
149
190
  return this;
150
191
  } catch (cause) {
151
192
  return observerThrowToDefect(cause, this.error);
@@ -154,7 +195,8 @@ var Res = class {
154
195
  flatTapErr(f) {
155
196
  if (this.tag !== "Err") return this;
156
197
  try {
157
- const r = f(this.error);
198
+ const r = runMatch(f, this.error);
199
+ if (!isResult(r)) return nonResultCallbackDefect();
158
200
  return r.tag === "Ok" ? this : passThrough(r);
159
201
  } catch (cause) {
160
202
  return observerThrowToDefect(cause, this.error);
@@ -163,7 +205,8 @@ var Res = class {
163
205
  recoverDefect(f) {
164
206
  if (this.tag !== "Defect") return this;
165
207
  try {
166
- return f(this.cause);
208
+ const r = f(this.cause);
209
+ return isResult(r) ? r : nonResultCallbackDefect();
167
210
  } catch (cause) {
168
211
  return defectRes(cause);
169
212
  }
@@ -189,50 +232,34 @@ var Res = class {
189
232
  match(cases) {
190
233
  switch (this.tag) {
191
234
  case "Ok": return cases.ok(this.value);
192
- case "Err": return cases.err(this.error);
235
+ case "Err": return cases.err((0, ts_pattern.match)(this.error)).run();
193
236
  case "Defect": return cases.defect(this.cause);
194
237
  }
195
238
  }
196
239
  get() {
197
240
  switch (this.tag) {
198
241
  case "Ok": return this.value;
199
- case "Err": throw new UnwrapError(this.error);
242
+ case "Err": throw new GetError(this.error);
200
243
  case "Defect": throw this.cause;
201
244
  }
202
245
  }
203
- /** @deprecated Use {@link Res.get}. */
204
- unwrap() {
205
- return this.get();
206
- }
207
246
  getErr() {
208
247
  switch (this.tag) {
209
248
  case "Err": return this.error;
210
- case "Ok": throw new UnwrapError(this.value);
249
+ case "Ok": throw new GetError(this.value);
211
250
  case "Defect": throw this.cause;
212
251
  }
213
252
  }
214
- /** @deprecated Use {@link Res.getErr}. */
215
- unwrapErr() {
216
- return this.getErr();
217
- }
218
253
  getOr(fallback) {
219
254
  if (this.tag === "Ok") return this.value;
220
255
  if (this.tag === "Defect") throw this.cause;
221
256
  return fallback;
222
257
  }
223
- /** @deprecated Use {@link Res.getOr}. */
224
- unwrapOr(fallback) {
225
- return this.getOr(fallback);
226
- }
227
258
  getOrElse(f) {
228
259
  if (this.tag === "Ok") return this.value;
229
260
  if (this.tag === "Defect") throw this.cause;
230
261
  return f(this.error);
231
262
  }
232
- /** @deprecated Use {@link Res.getOrElse}. */
233
- unwrapOrElse(f) {
234
- return this.getOrElse(f);
235
- }
236
263
  getOrNull() {
237
264
  if (this.tag === "Ok") return this.value;
238
265
  if (this.tag === "Defect") throw this.cause;
@@ -260,7 +287,19 @@ var Res = class {
260
287
  return new AsyncRes(Promise.resolve(this));
261
288
  }
262
289
  };
290
+ /**
291
+ * Cross-copy brand: `Symbol.for` yields the same symbol in every copy of the
292
+ * library (the dual CJS/ESM build, a duplicated install, another realm), so
293
+ * {@link isResult} can recognise a `Result` built by another copy whose `Res`
294
+ * class fails the `instanceof` check. Defined non-enumerably on the prototype,
295
+ * before it is frozen.
296
+ *
297
+ * @internal
298
+ */
299
+ const RESULT_BRAND = Symbol.for("unthrown.Result");
263
300
  const RESULT_PROTO = Res.prototype;
301
+ Object.defineProperty(RESULT_PROTO, RESULT_BRAND, { value: true });
302
+ Object.freeze(RESULT_PROTO);
264
303
  /**
265
304
  * Construct an `Ok` result — a plain object on the {@link Res} prototype.
266
305
  *
@@ -300,9 +339,12 @@ function defectRes(cause) {
300
339
  * @remarks
301
340
  * Unlike {@link isOk} / {@link isErr} / {@link isDefect}, which narrow a value
302
341
  * already known to be a `Result`, this narrows from `unknown` — useful at an
303
- * untyped boundary. It checks the value carries the `Result` prototype, so a
304
- * look-alike plain object (`{ tag: "Ok" }`) is **not** matched. An `AsyncResult`
305
- * is not a `Result` and returns `false`.
342
+ * untyped boundary. It checks the value carries the `Result` prototype
343
+ * (`instanceof` first, falling back to the `Symbol.for("unthrown.Result")`
344
+ * brand the prototype carries — so a `Result` built by **another copy** of
345
+ * unthrown, e.g. the CJS and ESM builds loaded side by side, is still
346
+ * recognised). A look-alike plain object (`{ tag: "Ok" }`) carries neither and
347
+ * is **not** matched. An `AsyncResult` is not a `Result` and returns `false`.
306
348
  *
307
349
  * @returns `true` when `x` is a `Result` produced by this library.
308
350
  *
@@ -321,7 +363,12 @@ function defectRes(cause) {
321
363
  * @category Guards
322
364
  */
323
365
  function isResult(x) {
324
- return x instanceof Res;
366
+ if (x instanceof Res) return true;
367
+ try {
368
+ return (typeof x === "object" || typeof x === "function") && x !== null && Reflect.get(x, RESULT_BRAND) === true;
369
+ } catch {
370
+ return false;
371
+ }
325
372
  }
326
373
  /**
327
374
  * Reuse a non-matching variant (an `Err` or `Defect`) as a differently-typed
@@ -337,6 +384,32 @@ function passThrough(self) {
337
384
  return self;
338
385
  }
339
386
  /**
387
+ * The Defect minted when a callback constrained to return a `Result` returns
388
+ * something else — reachable only from untyped/cast callers (in typed code the
389
+ * constraint is a compile error). The combinator-side sibling of the
390
+ * aggregates' non-`Result`-element guard: surface the out-of-contract value as
391
+ * a `Defect` here rather than letting a poison value throw a raw `TypeError`
392
+ * further down the pipeline.
393
+ *
394
+ * @internal
395
+ */
396
+ function nonResultCallbackDefect() {
397
+ return defectRes(/* @__PURE__ */ new TypeError("unthrown: a combinator callback returned a non-Result value"));
398
+ }
399
+ /**
400
+ * Drive an error-combinator callback: build `match(error)`, hand it (plus the
401
+ * injected `defect`) to the callback, and `.run()` the returned exhaustive
402
+ * builder to its output. `.run()` executes `.exhaustive()` — type-forced
403
+ * exhaustive, so it always matches for well-typed callers; a value that slips
404
+ * through the types (a widened cast, a JS caller) throws `NonExhaustiveError`,
405
+ * which the caller's `try/catch` turns into a `Defect` — an unmodeled failure.
406
+ *
407
+ * @internal
408
+ */
409
+ function runMatch(f, error) {
410
+ return f((0, ts_pattern.match)(error), defect).run();
411
+ }
412
+ /**
340
413
  * A throw inside a *failure observer* (`tapErr` / `tapDefect` / `flatTapErr`)
341
414
  * must not destroy the failure being observed — that is the exact place (e.g. a
342
415
  * failing error-logger) where losing the underlying failure hurts most. The
@@ -378,15 +451,15 @@ function scopeOf(value) {
378
451
  * @internal
379
452
  */
380
453
  var AsyncRes = class AsyncRes {
381
- promise;
454
+ #promise;
382
455
  constructor(promise) {
383
- this.promise = promise;
456
+ this.#promise = promise;
384
457
  }
385
458
  then(onfulfilled, onrejected) {
386
- return this.promise.then(onfulfilled, onrejected);
459
+ return this.#promise.then(onfulfilled, onrejected);
387
460
  }
388
461
  map(f) {
389
- return new AsyncRes(this.promise.then((r) => {
462
+ return new AsyncRes(this.#promise.then((r) => {
390
463
  if (r.tag !== "Ok") return passThrough(r);
391
464
  try {
392
465
  return okRes(f(r.value));
@@ -396,17 +469,18 @@ var AsyncRes = class AsyncRes {
396
469
  }));
397
470
  }
398
471
  flatMap(f) {
399
- return new AsyncRes(this.promise.then(async (r) => {
472
+ return new AsyncRes(this.#promise.then(async (r) => {
400
473
  if (r.tag !== "Ok") return passThrough(r);
401
474
  try {
402
- return await f(r.value);
475
+ const inner = await f(r.value);
476
+ return isResult(inner) ? inner : nonResultCallbackDefect();
403
477
  } catch (cause) {
404
478
  return defectRes(cause);
405
479
  }
406
480
  }));
407
481
  }
408
482
  tap(f) {
409
- return new AsyncRes(this.promise.then((r) => {
483
+ return new AsyncRes(this.#promise.then((r) => {
410
484
  if (r.tag !== "Ok") return r;
411
485
  try {
412
486
  f(r.value);
@@ -417,10 +491,11 @@ var AsyncRes = class AsyncRes {
417
491
  }));
418
492
  }
419
493
  flatTap(f) {
420
- return new AsyncRes(this.promise.then(async (r) => {
494
+ return new AsyncRes(this.#promise.then(async (r) => {
421
495
  if (r.tag !== "Ok") return passThrough(r);
422
496
  try {
423
497
  const inner = await f(r.value);
498
+ if (!isResult(inner)) return nonResultCallbackDefect();
424
499
  return inner.tag === "Ok" ? r : passThrough(inner);
425
500
  } catch (cause) {
426
501
  return defectRes(cause);
@@ -428,10 +503,11 @@ var AsyncRes = class AsyncRes {
428
503
  }));
429
504
  }
430
505
  bind(name, f) {
431
- return new AsyncRes(this.promise.then(async (r) => {
506
+ return new AsyncRes(this.#promise.then(async (r) => {
432
507
  if (r.tag !== "Ok") return passThrough(r);
433
508
  try {
434
509
  const inner = await f(r.value);
510
+ if (!isResult(inner)) return nonResultCallbackDefect();
435
511
  if (inner.tag !== "Ok") return passThrough(inner);
436
512
  return okRes({
437
513
  ...scopeOf(r.value),
@@ -443,7 +519,7 @@ var AsyncRes = class AsyncRes {
443
519
  }));
444
520
  }
445
521
  let(name, f) {
446
- return new AsyncRes(this.promise.then((r) => {
522
+ return new AsyncRes(this.#promise.then((r) => {
447
523
  if (r.tag !== "Ok") return passThrough(r);
448
524
  try {
449
525
  return okRes({
@@ -456,54 +532,64 @@ var AsyncRes = class AsyncRes {
456
532
  }));
457
533
  }
458
534
  as(value) {
459
- return new AsyncRes(this.promise.then((r) => r.tag === "Ok" ? okRes(value) : passThrough(r)));
535
+ return new AsyncRes(this.#promise.then((r) => r.tag === "Ok" ? okRes(value) : passThrough(r)));
460
536
  }
461
537
  discard() {
462
- return new AsyncRes(this.promise.then((r) => r.tag === "Ok" ? okRes(void 0) : passThrough(r)));
538
+ return new AsyncRes(this.#promise.then((r) => r.tag === "Ok" ? okRes(void 0) : passThrough(r)));
539
+ }
540
+ ensure(predicate, onFail) {
541
+ return new AsyncRes(this.#promise.then((r) => {
542
+ if (r.tag !== "Ok") return passThrough(r);
543
+ try {
544
+ return predicate(r.value) ? r : errRes(onFail(r.value));
545
+ } catch (cause) {
546
+ return defectRes(cause);
547
+ }
548
+ }));
463
549
  }
464
550
  mapErr(f) {
465
- return new AsyncRes(this.promise.then((r) => {
551
+ return new AsyncRes(this.#promise.then((r) => {
466
552
  if (r.tag !== "Err") return passThrough(r);
467
553
  try {
468
- return errRes(f(r.error));
554
+ const out = runMatch(f, r.error);
555
+ if (isDefectMarker(out)) return defectRes(out.cause);
556
+ return errRes(out);
469
557
  } catch (cause) {
470
558
  return defectRes(cause);
471
559
  }
472
560
  }));
473
561
  }
474
562
  flatMapErr(f) {
475
- return new AsyncRes(this.promise.then(async (r) => {
563
+ return new AsyncRes(this.#promise.then(async (r) => {
476
564
  if (r.tag !== "Err") return passThrough(r);
477
565
  try {
478
- return await f(r.error);
566
+ const out = runMatch(f, r.error);
567
+ if (isDefectMarker(out)) return defectRes(out.cause);
568
+ const inner = await out;
569
+ if (!isResult(inner)) return nonResultCallbackDefect();
570
+ return inner;
479
571
  } catch (cause) {
480
572
  return defectRes(cause);
481
573
  }
482
574
  }));
483
575
  }
484
- /** @deprecated Use {@link AsyncRes.flatMapErr}. */
485
- orElse(f) {
486
- return this.flatMapErr(f);
487
- }
488
576
  recoverErr(f) {
489
- return new AsyncRes(this.promise.then((r) => {
577
+ return new AsyncRes(this.#promise.then((r) => {
490
578
  if (r.tag !== "Err") return passThrough(r);
491
579
  try {
492
- return okRes(f(r.error));
580
+ const out = runMatch(f, r.error);
581
+ if (isDefectMarker(out)) return defectRes(out.cause);
582
+ return okRes(out);
493
583
  } catch (cause) {
494
584
  return defectRes(cause);
495
585
  }
496
586
  }));
497
587
  }
498
- /** @deprecated Use {@link AsyncRes.recoverErr}. */
499
- recover(f) {
500
- return this.recoverErr(f);
501
- }
502
588
  tapErr(f) {
503
- return new AsyncRes(this.promise.then((r) => {
589
+ return new AsyncRes(this.#promise.then((r) => {
504
590
  if (r.tag !== "Err") return r;
505
591
  try {
506
- f(r.error);
592
+ runMatch(f, r.error);
507
593
  return r;
508
594
  } catch (cause) {
509
595
  return observerThrowToDefect(cause, r.error);
@@ -511,10 +597,11 @@ var AsyncRes = class AsyncRes {
511
597
  }));
512
598
  }
513
599
  flatTapErr(f) {
514
- return new AsyncRes(this.promise.then(async (r) => {
600
+ return new AsyncRes(this.#promise.then(async (r) => {
515
601
  if (r.tag !== "Err") return passThrough(r);
516
602
  try {
517
- const inner = await f(r.error);
603
+ const inner = await runMatch(f, r.error);
604
+ if (!isResult(inner)) return nonResultCallbackDefect();
518
605
  return inner.tag === "Ok" ? passThrough(r) : passThrough(inner);
519
606
  } catch (cause) {
520
607
  return observerThrowToDefect(cause, r.error);
@@ -522,17 +609,18 @@ var AsyncRes = class AsyncRes {
522
609
  }));
523
610
  }
524
611
  recoverDefect(f) {
525
- return new AsyncRes(this.promise.then(async (r) => {
612
+ return new AsyncRes(this.#promise.then(async (r) => {
526
613
  if (r.tag !== "Defect") return r;
527
614
  try {
528
- return await f(r.cause);
615
+ const inner = await f(r.cause);
616
+ return isResult(inner) ? inner : nonResultCallbackDefect();
529
617
  } catch (cause) {
530
618
  return defectRes(cause);
531
619
  }
532
620
  }));
533
621
  }
534
622
  tapDefect(f) {
535
- return new AsyncRes(this.promise.then((r) => {
623
+ return new AsyncRes(this.#promise.then((r) => {
536
624
  if (r.tag !== "Defect") return r;
537
625
  try {
538
626
  f(r.cause);
@@ -543,7 +631,7 @@ var AsyncRes = class AsyncRes {
543
631
  }));
544
632
  }
545
633
  tapFailure(f) {
546
- return new AsyncRes(this.promise.then((r) => {
634
+ return new AsyncRes(this.#promise.then((r) => {
547
635
  if (r.tag === "Ok") return r;
548
636
  try {
549
637
  f(r);
@@ -554,46 +642,31 @@ var AsyncRes = class AsyncRes {
554
642
  }));
555
643
  }
556
644
  match(cases) {
557
- return this.promise.then((r) => r.match(cases));
645
+ return this.#promise.then((r) => r.match(cases));
558
646
  }
559
647
  get() {
560
- return this.promise.then((r) => r.get());
561
- }
562
- /** @deprecated Use {@link AsyncRes.get}. */
563
- unwrap() {
564
- return this.get();
648
+ return this.#promise.then((r) => r.get());
565
649
  }
566
650
  getErr() {
567
- return this.promise.then((r) => r.getErr());
568
- }
569
- /** @deprecated Use {@link AsyncRes.getErr}. */
570
- unwrapErr() {
571
- return this.getErr();
651
+ return this.#promise.then((r) => r.getErr());
572
652
  }
573
653
  getOr(fallback) {
574
- return this.promise.then((r) => r.getOr(fallback));
575
- }
576
- /** @deprecated Use {@link AsyncRes.getOr}. */
577
- unwrapOr(fallback) {
578
- return this.getOr(fallback);
654
+ return this.#promise.then((r) => r.getOr(fallback));
579
655
  }
580
656
  getOrElse(f) {
581
- return this.promise.then((r) => r.getOrElse(f));
582
- }
583
- /** @deprecated Use {@link AsyncRes.getOrElse}. */
584
- unwrapOrElse(f) {
585
- return this.getOrElse(f);
657
+ return this.#promise.then((r) => r.getOrElse(f));
586
658
  }
587
659
  getOrNull() {
588
- return this.promise.then((r) => r.getOrNull());
660
+ return this.#promise.then((r) => r.getOrNull());
589
661
  }
590
662
  getOrUndefined() {
591
- return this.promise.then((r) => r.getOrUndefined());
663
+ return this.#promise.then((r) => r.getOrUndefined());
592
664
  }
593
665
  getOrThrow() {
594
- return this.promise.then((r) => r.getOrThrow());
666
+ return this.#promise.then((r) => r.getOrThrow());
595
667
  }
596
668
  };
669
+ Object.freeze(AsyncRes.prototype);
597
670
  //#endregion
598
671
  //#region src/constructors.ts
599
672
  function Ok(value) {
@@ -759,35 +832,32 @@ function isDefect(r) {
759
832
  function Do() {
760
833
  return Ok({});
761
834
  }
762
- //#endregion
763
- //#region src/defect.ts
764
- const DEFECT = Symbol("unthrown/Defect");
765
835
  /**
766
- * Wrap a cause as a `Defect` marker — the value returned from a `qualify`
767
- * function when a failure is **not** a modeled domain error. The boundary
768
- * (`fromPromise` / `fromThrowable`) passes this in as `qualify`'s second
769
- * argument, so domain code never imports it.
836
+ * Start an **asynchronous** do-notation chain with an empty object scope — the
837
+ * pre-lifted form of {@link Do}, sparing you `Do().toAsync()`.
770
838
  *
771
- * @param cause - the original thrown/rejected value.
772
- * @returns an opaque Defect marker carrying `cause`.
839
+ * @remarks
840
+ * From here a `bind` may return a `Result` **or** an `AsyncResult`; the scope
841
+ * accumulates exactly as in a sync {@link Do} chain, and a throw in any step
842
+ * becomes a `Defect`. Named with the `Async` suffix the async free functions
843
+ * carry (`OkAsync`, `allAsync`); the {@link AsyncResult} companion aliases it as
844
+ * `AsyncResult.Do` (the namespace already says "async", so the suffix drops).
773
845
  *
774
- * @internal
775
- */
776
- function defect(cause) {
777
- return {
778
- [DEFECT]: true,
779
- cause
780
- };
781
- }
782
- /**
783
- * Internal guard for the qualify-time marker. Distinct from the public
784
- * {@link isDefect} state guard — this one narrows the `E | Defect` union a
785
- * `qualify` function returns, not a `Result`.
846
+ * @example
847
+ * ```ts
848
+ * import { DoAsync, Ok } from "unthrown";
786
849
  *
787
- * @internal
850
+ * const result = await DoAsync()
851
+ * .bind("user", () => findUser(id)) // AsyncResult<User, NotFound>
852
+ * .bind("plan", ({ user }) => Ok(user.plan)) // a sync Result is accepted too
853
+ * .let("label", ({ user, plan }) => `${user.name} on ${plan}`);
854
+ * // Result<{ user: User; plan: Plan; label: string }, NotFound>
855
+ * ```
856
+ *
857
+ * @category Do-notation
788
858
  */
789
- function isDefectMarker(x) {
790
- return typeof x === "object" && x !== null && x[DEFECT] === true;
859
+ function DoAsync() {
860
+ return Do().toAsync();
791
861
  }
792
862
  //#endregion
793
863
  //#region src/interop.ts
@@ -827,7 +897,10 @@ function fromNullable(value, onAbsent) {
827
897
  * `qualify` **must** triage every thrown cause into a modeled error `E` or a
828
898
  * `Defect` (via the injected `defect` helper, its second argument) — there is no
829
899
  * path that leaves `unknown` in `E`. A throw inside `qualify` itself is treated
830
- * as a `Defect`.
900
+ * as a `Defect`. `qualify` is **synchronous**: an `async` qualify is rejected at
901
+ * compile time ({@link NotThenable}) — its `Promise` would land in `E` un-triaged
902
+ * — and a thenable slipped past the types at runtime becomes a `Defect` (never
903
+ * an `Err(Promise)`), its orphaned rejection silenced.
831
904
  *
832
905
  * The modeled error type is `Exclude<R, Defect>` — the `Defect` arm of
833
906
  * `qualify`'s return is **subtracted** from `E`, never inferred into it. So a
@@ -917,7 +990,10 @@ function fromSafeThrowable(fn) {
917
990
  * `qualify` **must** map each rejection cause into a modeled error `E` or a
918
991
  * `Defect` (via the injected `defect` helper, its second argument). The returned
919
992
  * `AsyncResult`'s internal promise never rejects; `await`-ing it always yields a
920
- * `Result`. A throw inside `qualify` is itself a `Defect`.
993
+ * `Result`. A throw inside `qualify` is itself a `Defect`. `qualify` is
994
+ * **synchronous**: an `async` qualify is rejected at compile time
995
+ * ({@link NotThenable}), and a thenable slipped past the types at runtime
996
+ * becomes a `Defect` (never an `Err(Promise)`), its orphaned rejection silenced.
921
997
  *
922
998
  * The modeled error type is `Exclude<R, Defect>` — the `Defect` arm of
923
999
  * `qualify`'s return is **subtracted** from `E`, never inferred into it. So a
@@ -980,26 +1056,50 @@ function fromSafePromise(promise) {
980
1056
  function qualifyToResult(cause, qualify) {
981
1057
  try {
982
1058
  const q = qualify(cause, defect);
983
- return isDefectMarker(q) ? defectRes(q.cause) : errRes(q);
1059
+ if (isDefectMarker(q)) return defectRes(q.cause);
1060
+ if (isThenable(q)) {
1061
+ Promise.resolve(q).then(void 0, () => void 0);
1062
+ return defectRes(/* @__PURE__ */ new TypeError("unthrown: qualify must be synchronous — it returned a thenable; triage the cause without awaiting"));
1063
+ }
1064
+ return errRes(q);
984
1065
  } catch (qErr) {
985
1066
  return defectRes(qErr);
986
1067
  }
987
1068
  }
988
1069
  /**
1070
+ * Runtime thenable probe for the belt-and-braces guard above. Called inside the
1071
+ * caller's `try`, so even a hostile `.then` getter lands on the Defect path.
1072
+ *
1073
+ * @internal
1074
+ */
1075
+ function isThenable(x) {
1076
+ return (typeof x === "object" || typeof x === "function") && x !== null && typeof x.then === "function";
1077
+ }
1078
+ /**
989
1079
  * Fold an array of settled `Result`s: first `Err` wins, any `Defect` dominates,
990
1080
  * else `Ok` of the values array.
991
1081
  *
992
1082
  * @internal
993
1083
  */
1084
+ /** The Defect minted for an out-of-contract non-`Result` element in an aggregate. */
1085
+ function nonResultDefect() {
1086
+ return defectRes(/* @__PURE__ */ new TypeError("unthrown: aggregate received a non-Result element"));
1087
+ }
994
1088
  function foldArray(results) {
995
1089
  let firstErr;
996
1090
  let firstDefect;
997
1091
  const values = [];
998
- for (const r of results) if (r.tag === "Defect") {
999
- firstDefect ??= r;
1000
- break;
1001
- } else if (r.tag === "Err") firstErr ??= r;
1002
- else values.push(r.value);
1092
+ for (const r of results) {
1093
+ if (!isResult(r)) {
1094
+ firstDefect ??= nonResultDefect();
1095
+ break;
1096
+ }
1097
+ if (r.tag === "Defect") {
1098
+ firstDefect ??= r;
1099
+ break;
1100
+ } else if (r.tag === "Err") firstErr ??= r;
1101
+ else values.push(r.value);
1102
+ }
1003
1103
  return firstDefect ?? firstErr ?? Ok(values);
1004
1104
  }
1005
1105
  /**
@@ -1013,16 +1113,22 @@ function foldRecord(results) {
1013
1113
  let firstErr;
1014
1114
  let firstDefect;
1015
1115
  const values = {};
1016
- for (const [key, r] of Object.entries(results)) if (r.tag === "Defect") {
1017
- firstDefect ??= r;
1018
- break;
1019
- } else if (r.tag === "Err") firstErr ??= r;
1020
- else Object.defineProperty(values, key, {
1021
- value: r.value,
1022
- enumerable: true,
1023
- writable: true,
1024
- configurable: true
1025
- });
1116
+ for (const [key, r] of Object.entries(results)) {
1117
+ if (!isResult(r)) {
1118
+ firstDefect ??= nonResultDefect();
1119
+ break;
1120
+ }
1121
+ if (r.tag === "Defect") {
1122
+ firstDefect ??= r;
1123
+ break;
1124
+ } else if (r.tag === "Err") firstErr ??= r;
1125
+ else Object.defineProperty(values, key, {
1126
+ value: r.value,
1127
+ enumerable: true,
1128
+ writable: true,
1129
+ configurable: true
1130
+ });
1131
+ }
1026
1132
  return firstDefect ?? firstErr ?? Ok(values);
1027
1133
  }
1028
1134
  /**
@@ -1094,7 +1200,7 @@ function allFromDict(results) {
1094
1200
  * ```
1095
1201
  */
1096
1202
  function allAsync(results) {
1097
- return new AsyncRes(Promise.all(results).then((resolved) => foldArray(resolved)));
1203
+ return new AsyncRes(Promise.all(results.map((r) => Promise.resolve(r).then((x) => x, (cause) => defectRes(cause)))).then((resolved) => foldArray(resolved)));
1098
1204
  }
1099
1205
  /**
1100
1206
  * The asynchronous counterpart of {@link allFromDict}: combine a record of
@@ -1119,7 +1225,7 @@ function allAsync(results) {
1119
1225
  */
1120
1226
  function allFromDictAsync(results) {
1121
1227
  const entries = Object.entries(results);
1122
- return new AsyncRes(Promise.all(entries.map(([, ar]) => ar)).then((resolved) => {
1228
+ return new AsyncRes(Promise.all(entries.map(([, ar]) => Promise.resolve(ar).then((x) => x, (cause) => defectRes(cause)))).then((resolved) => {
1123
1229
  const byKey = Object.create(null);
1124
1230
  entries.forEach(([key], i) => {
1125
1231
  byKey[key] = resolved[i];
@@ -1172,8 +1278,9 @@ const Result = {
1172
1278
  /**
1173
1279
  * Companion object grouping the **`AsyncResult`-producing** entry points under
1174
1280
  * the matching namespace: {@link AsyncResult.Ok}, {@link AsyncResult.Err},
1175
- * {@link AsyncResult.fromPromise}, {@link AsyncResult.fromSafePromise},
1176
- * {@link AsyncResult.all}, {@link AsyncResult.allFromDict}.
1281
+ * {@link AsyncResult.Do}, {@link AsyncResult.fromPromise},
1282
+ * {@link AsyncResult.fromSafePromise}, {@link AsyncResult.all},
1283
+ * {@link AsyncResult.allFromDict}.
1177
1284
  *
1178
1285
  * @remarks
1179
1286
  * The async sibling of {@link Result}. Statics are grouped by what they
@@ -1181,7 +1288,8 @@ const Result = {
1181
1288
  * and the async aggregates sit here rather than on {@link Result}; the namespace
1182
1289
  * already conveys "async", so the members drop the `Async` suffix their free
1183
1290
  * functions carry (`AsyncResult.Ok` is `OkAsync`; `AsyncResult.Err` is
1184
- * `ErrAsync`; `AsyncResult.all` is `allAsync`; `AsyncResult.allFromDict` is
1291
+ * `ErrAsync`; `AsyncResult.Do` is `DoAsync`; `AsyncResult.all` is `allAsync`;
1292
+ * `AsyncResult.allFromDict` is
1185
1293
  * `allFromDictAsync`). Like {@link Result}, the free functions remain the
1186
1294
  * primary, tree-shakeable API; the value `AsyncResult` and the type
1187
1295
  * {@link AsyncResult} share one name.
@@ -1198,6 +1306,7 @@ const Result = {
1198
1306
  const AsyncResult = {
1199
1307
  Ok: OkAsync,
1200
1308
  Err: ErrAsync,
1309
+ Do: DoAsync,
1201
1310
  fromPromise,
1202
1311
  fromSafePromise,
1203
1312
  all: allAsync,
@@ -1221,10 +1330,16 @@ const AsyncResult = {
1221
1330
  * `tag` and cannot be overridden by the payload. `name` is likewise reserved —
1222
1331
  * it is the display label (set it with `options.name`); a payload `name` is
1223
1332
  * rejected at compile time (and excluded from the instance type), so it can't
1224
- * shadow `Error.name`.
1225
- *
1226
- * `_tag` is the discriminant used by {@link matchTags}; `Error.name` is the
1227
- * human-facing label in stack traces and logs. By default they coincide, but
1333
+ * shadow `Error.name`. `stack` is reserved the same way — it is `Error`'s
1334
+ * trace, and even an untyped payload `stack` cannot clobber the real one.
1335
+ * `cause` is deliberately **not** reserved: `Error.cause` is typed `unknown`,
1336
+ * so a payload `cause` (e.g. a wrapped driver error) is a legitimate,
1337
+ * *narrowing* structured field.
1338
+ *
1339
+ * `_tag` is the discriminant matched by {@link tag} in the error combinators
1340
+ * (`result.mapErr((matcher) => matcher.with(tag("NotFound"), …))`) and in
1341
+ * `match`; `Error.name` is the human-facing label in stack traces and logs. By
1342
+ * default they coincide, but
1228
1343
  * they can be **decoupled** with `options.name` — so a tag can be namespaced for
1229
1344
  * collision-safety (`"@my-lib/RetryableError"`) without that slash-prefixed
1230
1345
  * string leaking into `Error.name`:
@@ -1264,7 +1379,17 @@ function TaggedError(tag, options) {
1264
1379
  _tag;
1265
1380
  constructor(props) {
1266
1381
  super();
1267
- if (props) Object.assign(this, props);
1382
+ if (props) {
1383
+ const stack = this.stack;
1384
+ Object.assign(this, props);
1385
+ delete this.stack;
1386
+ if (stack !== void 0) Object.defineProperty(this, "stack", {
1387
+ value: stack,
1388
+ writable: true,
1389
+ enumerable: false,
1390
+ configurable: true
1391
+ });
1392
+ }
1268
1393
  this._tag = tag;
1269
1394
  this.name = displayName;
1270
1395
  delete this.message;
@@ -1273,28 +1398,46 @@ function TaggedError(tag, options) {
1273
1398
  }
1274
1399
  return TaggedErrorBase;
1275
1400
  }
1276
- function matchTags(result, handlers) {
1277
- const onErr = (error) => {
1278
- const tag = error._tag;
1279
- const handler = tag === "Ok" || tag === "Defect" || !Object.hasOwn(handlers, tag) ? void 0 : handlers[tag];
1280
- return handler ? handler(error) : handlers.Defect(error);
1281
- };
1282
- return result.match({
1283
- ok: handlers.Ok,
1284
- err: onErr,
1285
- defect: handlers.Defect
1286
- });
1401
+ /**
1402
+ * A `ts-pattern` pattern matching any value whose `_tag` equals `value` — a
1403
+ * {@link TaggedError}, or any discriminated member. Equivalent to the object
1404
+ * pattern `{ _tag: value }`, but reads better inside an error-matching
1405
+ * combinator and narrows to the matching variant, payload included.
1406
+ *
1407
+ * @typeParam Tag - the string literal tag to match.
1408
+ * @param value - the `_tag` to match.
1409
+ *
1410
+ * @category Tagged errors
1411
+ *
1412
+ * @example
1413
+ * ```ts
1414
+ * result.mapErr((matcher) =>
1415
+ * matcher
1416
+ * .with(tag("NotFound"), () => new NotFoundException())
1417
+ * .with(tag("Conflict"), (e) => new ConflictException(e.key)),
1418
+ * );
1419
+ * ```
1420
+ */
1421
+ function tag(value) {
1422
+ return { _tag: value };
1287
1423
  }
1288
1424
  //#endregion
1289
1425
  exports.AsyncResult = AsyncResult;
1290
1426
  exports.Do = Do;
1427
+ exports.DoAsync = DoAsync;
1291
1428
  exports.Err = Err;
1292
1429
  exports.ErrAsync = ErrAsync;
1430
+ exports.GetError = GetError;
1293
1431
  exports.Ok = Ok;
1294
1432
  exports.OkAsync = OkAsync;
1433
+ Object.defineProperty(exports, "P", {
1434
+ enumerable: true,
1435
+ get: function() {
1436
+ return ts_pattern.P;
1437
+ }
1438
+ });
1295
1439
  exports.Result = Result;
1296
1440
  exports.TaggedError = TaggedError;
1297
- exports.UnwrapError = UnwrapError;
1298
1441
  exports.all = all;
1299
1442
  exports.allAsync = allAsync;
1300
1443
  exports.allFromDict = allFromDict;
@@ -1308,4 +1451,10 @@ exports.isDefect = isDefect;
1308
1451
  exports.isErr = isErr;
1309
1452
  exports.isOk = isOk;
1310
1453
  exports.isResult = isResult;
1311
- exports.matchTags = matchTags;
1454
+ Object.defineProperty(exports, "match", {
1455
+ enumerable: true,
1456
+ get: function() {
1457
+ return ts_pattern.match;
1458
+ }
1459
+ });
1460
+ exports.tag = tag;