unthrown 4.3.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);
@@ -189,50 +218,34 @@ var Res = class {
189
218
  match(cases) {
190
219
  switch (this.tag) {
191
220
  case "Ok": return cases.ok(this.value);
192
- case "Err": return cases.err(this.error);
221
+ case "Err": return cases.err((0, ts_pattern.match)(this.error)).run();
193
222
  case "Defect": return cases.defect(this.cause);
194
223
  }
195
224
  }
196
225
  get() {
197
226
  switch (this.tag) {
198
227
  case "Ok": return this.value;
199
- case "Err": throw new UnwrapError(this.error);
228
+ case "Err": throw new GetError(this.error);
200
229
  case "Defect": throw this.cause;
201
230
  }
202
231
  }
203
- /** @deprecated Use {@link Res.get}. */
204
- unwrap() {
205
- return this.get();
206
- }
207
232
  getErr() {
208
233
  switch (this.tag) {
209
234
  case "Err": return this.error;
210
- case "Ok": throw new UnwrapError(this.value);
235
+ case "Ok": throw new GetError(this.value);
211
236
  case "Defect": throw this.cause;
212
237
  }
213
238
  }
214
- /** @deprecated Use {@link Res.getErr}. */
215
- unwrapErr() {
216
- return this.getErr();
217
- }
218
239
  getOr(fallback) {
219
240
  if (this.tag === "Ok") return this.value;
220
241
  if (this.tag === "Defect") throw this.cause;
221
242
  return fallback;
222
243
  }
223
- /** @deprecated Use {@link Res.getOr}. */
224
- unwrapOr(fallback) {
225
- return this.getOr(fallback);
226
- }
227
244
  getOrElse(f) {
228
245
  if (this.tag === "Ok") return this.value;
229
246
  if (this.tag === "Defect") throw this.cause;
230
247
  return f(this.error);
231
248
  }
232
- /** @deprecated Use {@link Res.getOrElse}. */
233
- unwrapOrElse(f) {
234
- return this.getOrElse(f);
235
- }
236
249
  getOrNull() {
237
250
  if (this.tag === "Ok") return this.value;
238
251
  if (this.tag === "Defect") throw this.cause;
@@ -337,6 +350,19 @@ function passThrough(self) {
337
350
  return self;
338
351
  }
339
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
+ /**
340
366
  * A throw inside a *failure observer* (`tapErr` / `tapDefect` / `flatTapErr`)
341
367
  * must not destroy the failure being observed — that is the exact place (e.g. a
342
368
  * failing error-logger) where losing the underlying failure hurts most. The
@@ -465,7 +491,9 @@ var AsyncRes = class AsyncRes {
465
491
  return new AsyncRes(this.promise.then((r) => {
466
492
  if (r.tag !== "Err") return passThrough(r);
467
493
  try {
468
- return errRes(f(r.error));
494
+ const out = runMatch(f, r.error);
495
+ if (isDefectMarker(out)) return defectRes(out.cause);
496
+ return errRes(out);
469
497
  } catch (cause) {
470
498
  return defectRes(cause);
471
499
  }
@@ -475,35 +503,31 @@ var AsyncRes = class AsyncRes {
475
503
  return new AsyncRes(this.promise.then(async (r) => {
476
504
  if (r.tag !== "Err") return passThrough(r);
477
505
  try {
478
- return await f(r.error);
506
+ const out = runMatch(f, r.error);
507
+ if (isDefectMarker(out)) return defectRes(out.cause);
508
+ return await out;
479
509
  } catch (cause) {
480
510
  return defectRes(cause);
481
511
  }
482
512
  }));
483
513
  }
484
- /** @deprecated Use {@link AsyncRes.flatMapErr}. */
485
- orElse(f) {
486
- return this.flatMapErr(f);
487
- }
488
514
  recoverErr(f) {
489
515
  return new AsyncRes(this.promise.then((r) => {
490
516
  if (r.tag !== "Err") return passThrough(r);
491
517
  try {
492
- return okRes(f(r.error));
518
+ const out = runMatch(f, r.error);
519
+ if (isDefectMarker(out)) return defectRes(out.cause);
520
+ return okRes(out);
493
521
  } catch (cause) {
494
522
  return defectRes(cause);
495
523
  }
496
524
  }));
497
525
  }
498
- /** @deprecated Use {@link AsyncRes.recoverErr}. */
499
- recover(f) {
500
- return this.recoverErr(f);
501
- }
502
526
  tapErr(f) {
503
527
  return new AsyncRes(this.promise.then((r) => {
504
528
  if (r.tag !== "Err") return r;
505
529
  try {
506
- f(r.error);
530
+ runMatch(f, r.error);
507
531
  return r;
508
532
  } catch (cause) {
509
533
  return observerThrowToDefect(cause, r.error);
@@ -514,7 +538,7 @@ var AsyncRes = class AsyncRes {
514
538
  return new AsyncRes(this.promise.then(async (r) => {
515
539
  if (r.tag !== "Err") return passThrough(r);
516
540
  try {
517
- const inner = await f(r.error);
541
+ const inner = await runMatch(f, r.error);
518
542
  return inner.tag === "Ok" ? passThrough(r) : passThrough(inner);
519
543
  } catch (cause) {
520
544
  return observerThrowToDefect(cause, r.error);
@@ -559,31 +583,15 @@ var AsyncRes = class AsyncRes {
559
583
  get() {
560
584
  return this.promise.then((r) => r.get());
561
585
  }
562
- /** @deprecated Use {@link AsyncRes.get}. */
563
- unwrap() {
564
- return this.get();
565
- }
566
586
  getErr() {
567
587
  return this.promise.then((r) => r.getErr());
568
588
  }
569
- /** @deprecated Use {@link AsyncRes.getErr}. */
570
- unwrapErr() {
571
- return this.getErr();
572
- }
573
589
  getOr(fallback) {
574
590
  return this.promise.then((r) => r.getOr(fallback));
575
591
  }
576
- /** @deprecated Use {@link AsyncRes.getOr}. */
577
- unwrapOr(fallback) {
578
- return this.getOr(fallback);
579
- }
580
592
  getOrElse(f) {
581
593
  return this.promise.then((r) => r.getOrElse(f));
582
594
  }
583
- /** @deprecated Use {@link AsyncRes.getOrElse}. */
584
- unwrapOrElse(f) {
585
- return this.getOrElse(f);
586
- }
587
595
  getOrNull() {
588
596
  return this.promise.then((r) => r.getOrNull());
589
597
  }
@@ -760,36 +768,6 @@ function Do() {
760
768
  return Ok({});
761
769
  }
762
770
  //#endregion
763
- //#region src/defect.ts
764
- const DEFECT = Symbol("unthrown/Defect");
765
- /**
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.
770
- *
771
- * @param cause - the original thrown/rejected value.
772
- * @returns an opaque Defect marker carrying `cause`.
773
- *
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`.
786
- *
787
- * @internal
788
- */
789
- function isDefectMarker(x) {
790
- return typeof x === "object" && x !== null && x[DEFECT] === true;
791
- }
792
- //#endregion
793
771
  //#region src/interop.ts
794
772
  /**
795
773
  * Bridge a nullable value into a {@link Result}: absence becomes a **modeled**
@@ -991,15 +969,25 @@ function qualifyToResult(cause, qualify) {
991
969
  *
992
970
  * @internal
993
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
+ }
994
976
  function foldArray(results) {
995
977
  let firstErr;
996
978
  let firstDefect;
997
979
  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);
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
+ }
1003
991
  return firstDefect ?? firstErr ?? Ok(values);
1004
992
  }
1005
993
  /**
@@ -1013,16 +1001,22 @@ function foldRecord(results) {
1013
1001
  let firstErr;
1014
1002
  let firstDefect;
1015
1003
  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
- });
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
+ }
1026
1020
  return firstDefect ?? firstErr ?? Ok(values);
1027
1021
  }
1028
1022
  /**
@@ -1094,7 +1088,7 @@ function allFromDict(results) {
1094
1088
  * ```
1095
1089
  */
1096
1090
  function allAsync(results) {
1097
- 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)));
1098
1092
  }
1099
1093
  /**
1100
1094
  * The asynchronous counterpart of {@link allFromDict}: combine a record of
@@ -1119,7 +1113,7 @@ function allAsync(results) {
1119
1113
  */
1120
1114
  function allFromDictAsync(results) {
1121
1115
  const entries = Object.entries(results);
1122
- 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) => {
1123
1117
  const byKey = Object.create(null);
1124
1118
  entries.forEach(([key], i) => {
1125
1119
  byKey[key] = resolved[i];
@@ -1223,8 +1217,10 @@ const AsyncResult = {
1223
1217
  * rejected at compile time (and excluded from the instance type), so it can't
1224
1218
  * shadow `Error.name`.
1225
1219
  *
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
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
1228
1224
  * they can be **decoupled** with `options.name` — so a tag can be namespaced for
1229
1225
  * collision-safety (`"@my-lib/RetryableError"`) without that slash-prefixed
1230
1226
  * string leaking into `Error.name`:
@@ -1273,28 +1269,45 @@ function TaggedError(tag, options) {
1273
1269
  }
1274
1270
  return TaggedErrorBase;
1275
1271
  }
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
- });
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 };
1287
1294
  }
1288
1295
  //#endregion
1289
1296
  exports.AsyncResult = AsyncResult;
1290
1297
  exports.Do = Do;
1291
1298
  exports.Err = Err;
1292
1299
  exports.ErrAsync = ErrAsync;
1300
+ exports.GetError = GetError;
1293
1301
  exports.Ok = Ok;
1294
1302
  exports.OkAsync = OkAsync;
1303
+ Object.defineProperty(exports, "P", {
1304
+ enumerable: true,
1305
+ get: function() {
1306
+ return ts_pattern.P;
1307
+ }
1308
+ });
1295
1309
  exports.Result = Result;
1296
1310
  exports.TaggedError = TaggedError;
1297
- exports.UnwrapError = UnwrapError;
1298
1311
  exports.all = all;
1299
1312
  exports.allAsync = allAsync;
1300
1313
  exports.allFromDict = allFromDict;
@@ -1308,4 +1321,10 @@ exports.isDefect = isDefect;
1308
1321
  exports.isErr = isErr;
1309
1322
  exports.isOk = isOk;
1310
1323
  exports.isResult = isResult;
1311
- 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;