unthrown 4.2.0 → 5.0.0-beta.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,7 @@ pnpm add unthrown
11
11
  ```
12
12
 
13
13
  ```ts
14
- import { fromPromise, TaggedError } from "unthrown";
14
+ import { fromPromise, P, TaggedError } from "unthrown";
15
15
 
16
16
  class NotFound extends TaggedError("NotFound") {} // our modeled domain failure
17
17
  class NotFoundError extends Error {} // what `fetchUser` rejects with on a 404
@@ -22,7 +22,7 @@ const user = fromPromise(fetchUser(id), (cause, defect) =>
22
22
 
23
23
  const status = await user.match({
24
24
  ok: () => 200,
25
- err: () => 404,
25
+ err: (matcher) => matcher.with(P._, () => 404), // `err` takes the exhaustive matcher
26
26
  defect: () => 500,
27
27
  });
28
28
  ```
@@ -32,8 +32,9 @@ const status = await user.match({
32
32
  observable only via `match` / `recoverDefect`.
33
33
  - **Qualification at every boundary** — `fromPromise` / `fromThrowable` force you
34
34
  to triage each failure into a modeled error or a defect.
35
- - **Tagged errors** — `TaggedError(tag)` + the exhaustive `matchTags` fold.
36
- - Zero runtime dependencies, ESM-first, dual CJS/ESM.
35
+ - **Tagged errors** — `TaggedError(tag)` + `tag(t)`, folded exhaustively through
36
+ `match`'s ts-pattern error matcher.
37
+ - One tiny runtime dependency (`ts-pattern`), ESM-first, dual CJS/ESM.
37
38
 
38
39
  See the [full documentation](https://btravstack.github.io/unthrown/) for the guide
39
40
  and complete API.
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 {
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
  }
@@ -113,7 +144,9 @@ var Res = class {
113
144
  mapErr(f) {
114
145
  if (this.tag !== "Err") return passThrough(this);
115
146
  try {
116
- return errRes(f(this.error));
147
+ const out = runMatch(f, this.error);
148
+ if (isDefectMarker(out)) return defectRes(out.cause);
149
+ return errRes(out);
117
150
  } catch (cause) {
118
151
  return defectRes(cause);
119
152
  }
@@ -121,31 +154,27 @@ var Res = class {
121
154
  flatMapErr(f) {
122
155
  if (this.tag !== "Err") return passThrough(this);
123
156
  try {
124
- return f(this.error);
157
+ const out = runMatch(f, this.error);
158
+ if (isDefectMarker(out)) return defectRes(out.cause);
159
+ return out;
125
160
  } catch (cause) {
126
161
  return defectRes(cause);
127
162
  }
128
163
  }
129
- /** @deprecated Use {@link Res.flatMapErr}. */
130
- orElse(f) {
131
- return this.flatMapErr(f);
132
- }
133
164
  recoverErr(f) {
134
165
  if (this.tag !== "Err") return passThrough(this);
135
166
  try {
136
- return okRes(f(this.error));
167
+ const out = runMatch(f, this.error);
168
+ if (isDefectMarker(out)) return defectRes(out.cause);
169
+ return okRes(out);
137
170
  } catch (cause) {
138
171
  return defectRes(cause);
139
172
  }
140
173
  }
141
- /** @deprecated Use {@link Res.recoverErr}. */
142
- recover(f) {
143
- return this.recoverErr(f);
144
- }
145
174
  tapErr(f) {
146
175
  if (this.tag !== "Err") return this;
147
176
  try {
148
- f(this.error);
177
+ runMatch(f, this.error);
149
178
  return this;
150
179
  } catch (cause) {
151
180
  return observerThrowToDefect(cause, this.error);
@@ -154,7 +183,7 @@ var Res = class {
154
183
  flatTapErr(f) {
155
184
  if (this.tag !== "Err") return this;
156
185
  try {
157
- const r = f(this.error);
186
+ const r = runMatch(f, this.error);
158
187
  return r.tag === "Ok" ? this : passThrough(r);
159
188
  } catch (cause) {
160
189
  return observerThrowToDefect(cause, this.error);
@@ -177,53 +206,46 @@ var Res = class {
177
206
  return observerThrowToDefect(cause, this.cause);
178
207
  }
179
208
  }
209
+ tapFailure(f) {
210
+ if (this.tag === "Ok") return this;
211
+ try {
212
+ f(this);
213
+ return this;
214
+ } catch (cause) {
215
+ return observerThrowToDefect(cause, this.tag === "Err" ? this.error : this.cause);
216
+ }
217
+ }
180
218
  match(cases) {
181
219
  switch (this.tag) {
182
220
  case "Ok": return cases.ok(this.value);
183
- case "Err": return cases.err(this.error);
221
+ case "Err": return cases.err((0, ts_pattern.match)(this.error)).run();
184
222
  case "Defect": return cases.defect(this.cause);
185
223
  }
186
224
  }
187
225
  get() {
188
226
  switch (this.tag) {
189
227
  case "Ok": return this.value;
190
- case "Err": throw new UnwrapError(this.error);
228
+ case "Err": throw new GetError(this.error);
191
229
  case "Defect": throw this.cause;
192
230
  }
193
231
  }
194
- /** @deprecated Use {@link Res.get}. */
195
- unwrap() {
196
- return this.get();
197
- }
198
232
  getErr() {
199
233
  switch (this.tag) {
200
234
  case "Err": return this.error;
201
- case "Ok": throw new UnwrapError(this.value);
235
+ case "Ok": throw new GetError(this.value);
202
236
  case "Defect": throw this.cause;
203
237
  }
204
238
  }
205
- /** @deprecated Use {@link Res.getErr}. */
206
- unwrapErr() {
207
- return this.getErr();
208
- }
209
239
  getOr(fallback) {
210
240
  if (this.tag === "Ok") return this.value;
211
241
  if (this.tag === "Defect") throw this.cause;
212
242
  return fallback;
213
243
  }
214
- /** @deprecated Use {@link Res.getOr}. */
215
- unwrapOr(fallback) {
216
- return this.getOr(fallback);
217
- }
218
244
  getOrElse(f) {
219
245
  if (this.tag === "Ok") return this.value;
220
246
  if (this.tag === "Defect") throw this.cause;
221
247
  return f(this.error);
222
248
  }
223
- /** @deprecated Use {@link Res.getOrElse}. */
224
- unwrapOrElse(f) {
225
- return this.getOrElse(f);
226
- }
227
249
  getOrNull() {
228
250
  if (this.tag === "Ok") return this.value;
229
251
  if (this.tag === "Defect") throw this.cause;
@@ -328,6 +350,19 @@ function passThrough(self) {
328
350
  return self;
329
351
  }
330
352
  /**
353
+ * Drive an error-combinator callback: build `match(error)`, hand it (plus the
354
+ * injected `defect`) to the callback, and `.run()` the returned exhaustive
355
+ * builder to its output. `.run()` executes `.exhaustive()` — type-forced
356
+ * exhaustive, so it always matches for well-typed callers; a value that slips
357
+ * through the types (a widened cast, a JS caller) throws `NonExhaustiveError`,
358
+ * which the caller's `try/catch` turns into a `Defect` — an unmodeled failure.
359
+ *
360
+ * @internal
361
+ */
362
+ function runMatch(f, error) {
363
+ return f((0, ts_pattern.match)(error), defect).run();
364
+ }
365
+ /**
331
366
  * A throw inside a *failure observer* (`tapErr` / `tapDefect` / `flatTapErr`)
332
367
  * must not destroy the failure being observed — that is the exact place (e.g. a
333
368
  * failing error-logger) where losing the underlying failure hurts most. The
@@ -456,7 +491,9 @@ var AsyncRes = class AsyncRes {
456
491
  return new AsyncRes(this.promise.then((r) => {
457
492
  if (r.tag !== "Err") return passThrough(r);
458
493
  try {
459
- return errRes(f(r.error));
494
+ const out = runMatch(f, r.error);
495
+ if (isDefectMarker(out)) return defectRes(out.cause);
496
+ return errRes(out);
460
497
  } catch (cause) {
461
498
  return defectRes(cause);
462
499
  }
@@ -466,35 +503,31 @@ var AsyncRes = class AsyncRes {
466
503
  return new AsyncRes(this.promise.then(async (r) => {
467
504
  if (r.tag !== "Err") return passThrough(r);
468
505
  try {
469
- return await f(r.error);
506
+ const out = runMatch(f, r.error);
507
+ if (isDefectMarker(out)) return defectRes(out.cause);
508
+ return await out;
470
509
  } catch (cause) {
471
510
  return defectRes(cause);
472
511
  }
473
512
  }));
474
513
  }
475
- /** @deprecated Use {@link AsyncRes.flatMapErr}. */
476
- orElse(f) {
477
- return this.flatMapErr(f);
478
- }
479
514
  recoverErr(f) {
480
515
  return new AsyncRes(this.promise.then((r) => {
481
516
  if (r.tag !== "Err") return passThrough(r);
482
517
  try {
483
- return okRes(f(r.error));
518
+ const out = runMatch(f, r.error);
519
+ if (isDefectMarker(out)) return defectRes(out.cause);
520
+ return okRes(out);
484
521
  } catch (cause) {
485
522
  return defectRes(cause);
486
523
  }
487
524
  }));
488
525
  }
489
- /** @deprecated Use {@link AsyncRes.recoverErr}. */
490
- recover(f) {
491
- return this.recoverErr(f);
492
- }
493
526
  tapErr(f) {
494
527
  return new AsyncRes(this.promise.then((r) => {
495
528
  if (r.tag !== "Err") return r;
496
529
  try {
497
- f(r.error);
530
+ runMatch(f, r.error);
498
531
  return r;
499
532
  } catch (cause) {
500
533
  return observerThrowToDefect(cause, r.error);
@@ -505,7 +538,7 @@ var AsyncRes = class AsyncRes {
505
538
  return new AsyncRes(this.promise.then(async (r) => {
506
539
  if (r.tag !== "Err") return passThrough(r);
507
540
  try {
508
- const inner = await f(r.error);
541
+ const inner = await runMatch(f, r.error);
509
542
  return inner.tag === "Ok" ? passThrough(r) : passThrough(inner);
510
543
  } catch (cause) {
511
544
  return observerThrowToDefect(cause, r.error);
@@ -533,37 +566,32 @@ var AsyncRes = class AsyncRes {
533
566
  }
534
567
  }));
535
568
  }
569
+ tapFailure(f) {
570
+ return new AsyncRes(this.promise.then((r) => {
571
+ if (r.tag === "Ok") return r;
572
+ try {
573
+ f(r);
574
+ return r;
575
+ } catch (cause) {
576
+ return observerThrowToDefect(cause, r.tag === "Err" ? r.error : r.cause);
577
+ }
578
+ }));
579
+ }
536
580
  match(cases) {
537
581
  return this.promise.then((r) => r.match(cases));
538
582
  }
539
583
  get() {
540
584
  return this.promise.then((r) => r.get());
541
585
  }
542
- /** @deprecated Use {@link AsyncRes.get}. */
543
- unwrap() {
544
- return this.get();
545
- }
546
586
  getErr() {
547
587
  return this.promise.then((r) => r.getErr());
548
588
  }
549
- /** @deprecated Use {@link AsyncRes.getErr}. */
550
- unwrapErr() {
551
- return this.getErr();
552
- }
553
589
  getOr(fallback) {
554
590
  return this.promise.then((r) => r.getOr(fallback));
555
591
  }
556
- /** @deprecated Use {@link AsyncRes.getOr}. */
557
- unwrapOr(fallback) {
558
- return this.getOr(fallback);
559
- }
560
592
  getOrElse(f) {
561
593
  return this.promise.then((r) => r.getOrElse(f));
562
594
  }
563
- /** @deprecated Use {@link AsyncRes.getOrElse}. */
564
- unwrapOrElse(f) {
565
- return this.getOrElse(f);
566
- }
567
595
  getOrNull() {
568
596
  return this.promise.then((r) => r.getOrNull());
569
597
  }
@@ -740,36 +768,6 @@ function Do() {
740
768
  return Ok({});
741
769
  }
742
770
  //#endregion
743
- //#region src/defect.ts
744
- const DEFECT = Symbol("unthrown/Defect");
745
- /**
746
- * Wrap a cause as a `Defect` marker — the value returned from a `qualify`
747
- * function when a failure is **not** a modeled domain error. The boundary
748
- * (`fromPromise` / `fromThrowable`) passes this in as `qualify`'s second
749
- * argument, so domain code never imports it.
750
- *
751
- * @param cause - the original thrown/rejected value.
752
- * @returns an opaque Defect marker carrying `cause`.
753
- *
754
- * @internal
755
- */
756
- function defect(cause) {
757
- return {
758
- [DEFECT]: true,
759
- cause
760
- };
761
- }
762
- /**
763
- * Internal guard for the qualify-time marker. Distinct from the public
764
- * {@link isDefect} state guard — this one narrows the `E | Defect` union a
765
- * `qualify` function returns, not a `Result`.
766
- *
767
- * @internal
768
- */
769
- function isDefectMarker(x) {
770
- return typeof x === "object" && x !== null && x[DEFECT] === true;
771
- }
772
- //#endregion
773
771
  //#region src/interop.ts
774
772
  /**
775
773
  * Bridge a nullable value into a {@link Result}: absence becomes a **modeled**
@@ -971,15 +969,25 @@ function qualifyToResult(cause, qualify) {
971
969
  *
972
970
  * @internal
973
971
  */
972
+ /** The Defect minted for an out-of-contract non-`Result` element in an aggregate. */
973
+ function nonResultDefect() {
974
+ return defectRes(/* @__PURE__ */ new TypeError("unthrown: aggregate received a non-Result element"));
975
+ }
974
976
  function foldArray(results) {
975
977
  let firstErr;
976
978
  let firstDefect;
977
979
  const values = [];
978
- for (const r of results) if (r.tag === "Defect") {
979
- firstDefect ??= r;
980
- break;
981
- } else if (r.tag === "Err") firstErr ??= r;
982
- else values.push(r.value);
980
+ for (const r of results) {
981
+ if (!isResult(r)) {
982
+ firstDefect ??= nonResultDefect();
983
+ break;
984
+ }
985
+ if (r.tag === "Defect") {
986
+ firstDefect ??= r;
987
+ break;
988
+ } else if (r.tag === "Err") firstErr ??= r;
989
+ else values.push(r.value);
990
+ }
983
991
  return firstDefect ?? firstErr ?? Ok(values);
984
992
  }
985
993
  /**
@@ -993,16 +1001,22 @@ function foldRecord(results) {
993
1001
  let firstErr;
994
1002
  let firstDefect;
995
1003
  const values = {};
996
- for (const [key, r] of Object.entries(results)) if (r.tag === "Defect") {
997
- firstDefect ??= r;
998
- break;
999
- } else if (r.tag === "Err") firstErr ??= r;
1000
- else Object.defineProperty(values, key, {
1001
- value: r.value,
1002
- enumerable: true,
1003
- writable: true,
1004
- configurable: true
1005
- });
1004
+ for (const [key, r] of Object.entries(results)) {
1005
+ if (!isResult(r)) {
1006
+ firstDefect ??= nonResultDefect();
1007
+ break;
1008
+ }
1009
+ if (r.tag === "Defect") {
1010
+ firstDefect ??= r;
1011
+ break;
1012
+ } else if (r.tag === "Err") firstErr ??= r;
1013
+ else Object.defineProperty(values, key, {
1014
+ value: r.value,
1015
+ enumerable: true,
1016
+ writable: true,
1017
+ configurable: true
1018
+ });
1019
+ }
1006
1020
  return firstDefect ?? firstErr ?? Ok(values);
1007
1021
  }
1008
1022
  /**
@@ -1074,7 +1088,7 @@ function allFromDict(results) {
1074
1088
  * ```
1075
1089
  */
1076
1090
  function allAsync(results) {
1077
- return new AsyncRes(Promise.all(results).then((resolved) => foldArray(resolved)));
1091
+ return new AsyncRes(Promise.all(results.map((r) => Promise.resolve(r).then((x) => x, (cause) => defectRes(cause)))).then((resolved) => foldArray(resolved)));
1078
1092
  }
1079
1093
  /**
1080
1094
  * The asynchronous counterpart of {@link allFromDict}: combine a record of
@@ -1099,7 +1113,7 @@ function allAsync(results) {
1099
1113
  */
1100
1114
  function allFromDictAsync(results) {
1101
1115
  const entries = Object.entries(results);
1102
- return new AsyncRes(Promise.all(entries.map(([, ar]) => ar)).then((resolved) => {
1116
+ return new AsyncRes(Promise.all(entries.map(([, ar]) => Promise.resolve(ar).then((x) => x, (cause) => defectRes(cause)))).then((resolved) => {
1103
1117
  const byKey = Object.create(null);
1104
1118
  entries.forEach(([key], i) => {
1105
1119
  byKey[key] = resolved[i];
@@ -1203,8 +1217,10 @@ const AsyncResult = {
1203
1217
  * rejected at compile time (and excluded from the instance type), so it can't
1204
1218
  * shadow `Error.name`.
1205
1219
  *
1206
- * `_tag` is the discriminant used by {@link matchTags}; `Error.name` is the
1207
- * human-facing label in stack traces and logs. By default they coincide, but
1220
+ * `_tag` is the discriminant matched by {@link tag} in the error combinators
1221
+ * (`result.mapErr((matcher) => matcher.with(tag("NotFound"), …))`) and in
1222
+ * `match`; `Error.name` is the human-facing label in stack traces and logs. By
1223
+ * default they coincide, but
1208
1224
  * they can be **decoupled** with `options.name` — so a tag can be namespaced for
1209
1225
  * collision-safety (`"@my-lib/RetryableError"`) without that slash-prefixed
1210
1226
  * string leaking into `Error.name`:
@@ -1253,28 +1269,45 @@ function TaggedError(tag, options) {
1253
1269
  }
1254
1270
  return TaggedErrorBase;
1255
1271
  }
1256
- function matchTags(result, handlers) {
1257
- const onErr = (error) => {
1258
- const tag = error._tag;
1259
- const handler = tag === "Ok" || tag === "Defect" || !Object.hasOwn(handlers, tag) ? void 0 : handlers[tag];
1260
- return handler ? handler(error) : handlers.Defect(error);
1261
- };
1262
- return result.match({
1263
- ok: handlers.Ok,
1264
- err: onErr,
1265
- defect: handlers.Defect
1266
- });
1272
+ /**
1273
+ * A `ts-pattern` pattern matching any value whose `_tag` equals `value` — a
1274
+ * {@link TaggedError}, or any discriminated member. Equivalent to the object
1275
+ * pattern `{ _tag: value }`, but reads better inside an error-matching
1276
+ * combinator and narrows to the matching variant, payload included.
1277
+ *
1278
+ * @typeParam Tag - the string literal tag to match.
1279
+ * @param value - the `_tag` to match.
1280
+ *
1281
+ * @category Tagged errors
1282
+ *
1283
+ * @example
1284
+ * ```ts
1285
+ * result.mapErr((matcher) =>
1286
+ * matcher
1287
+ * .with(tag("NotFound"), () => new NotFoundException())
1288
+ * .with(tag("Conflict"), (e) => new ConflictException(e.key)),
1289
+ * );
1290
+ * ```
1291
+ */
1292
+ function tag(value) {
1293
+ return { _tag: value };
1267
1294
  }
1268
1295
  //#endregion
1269
1296
  exports.AsyncResult = AsyncResult;
1270
1297
  exports.Do = Do;
1271
1298
  exports.Err = Err;
1272
1299
  exports.ErrAsync = ErrAsync;
1300
+ exports.GetError = GetError;
1273
1301
  exports.Ok = Ok;
1274
1302
  exports.OkAsync = OkAsync;
1303
+ Object.defineProperty(exports, "P", {
1304
+ enumerable: true,
1305
+ get: function() {
1306
+ return ts_pattern.P;
1307
+ }
1308
+ });
1275
1309
  exports.Result = Result;
1276
1310
  exports.TaggedError = TaggedError;
1277
- exports.UnwrapError = UnwrapError;
1278
1311
  exports.all = all;
1279
1312
  exports.allAsync = allAsync;
1280
1313
  exports.allFromDict = allFromDict;
@@ -1288,4 +1321,10 @@ exports.isDefect = isDefect;
1288
1321
  exports.isErr = isErr;
1289
1322
  exports.isOk = isOk;
1290
1323
  exports.isResult = isResult;
1291
- exports.matchTags = matchTags;
1324
+ Object.defineProperty(exports, "match", {
1325
+ enumerable: true,
1326
+ get: function() {
1327
+ return ts_pattern.match;
1328
+ }
1329
+ });
1330
+ exports.tag = tag;