unthrown 5.0.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -25,7 +25,7 @@ var NonExhaustiveError = class extends Error {
25
25
  constructor(input) {
26
26
  let printed;
27
27
  try {
28
- printed = JSON.stringify(input);
28
+ printed = JSON.stringify(input) ?? String(input);
29
29
  } catch {
30
30
  printed = String(input);
31
31
  }
@@ -261,7 +261,7 @@ var Res = class {
261
261
  if (this.tag !== "Ok") return passThrough(this);
262
262
  try {
263
263
  const r = f(this.value);
264
- return isResult(r) ? r : nonResultCallbackDefect();
264
+ return isResult(r) ? r : nonResultCallbackDefect(r);
265
265
  } catch (cause) {
266
266
  return defectRes(cause);
267
267
  }
@@ -269,7 +269,7 @@ var Res = class {
269
269
  tap(f) {
270
270
  if (this.tag !== "Ok") return this;
271
271
  try {
272
- f(this.value);
272
+ silenceIfThenable(f(this.value));
273
273
  return this;
274
274
  } catch (cause) {
275
275
  return defectRes(cause);
@@ -279,7 +279,7 @@ var Res = class {
279
279
  if (this.tag !== "Ok") return this;
280
280
  try {
281
281
  const r = f(this.value);
282
- if (!isResult(r)) return nonResultCallbackDefect();
282
+ if (!isResult(r)) return nonResultCallbackDefect(r);
283
283
  return r.tag === "Ok" ? this : passThrough(r);
284
284
  } catch (cause) {
285
285
  return defectRes(cause);
@@ -289,7 +289,7 @@ var Res = class {
289
289
  if (this.tag !== "Ok") return passThrough(this);
290
290
  try {
291
291
  const r = f(this.value);
292
- if (!isResult(r)) return nonResultCallbackDefect();
292
+ if (!isResult(r)) return nonResultCallbackDefect(r);
293
293
  if (r.tag !== "Ok") return passThrough(r);
294
294
  return okRes({
295
295
  ...scopeOf(this.value),
@@ -341,7 +341,7 @@ var Res = class {
341
341
  try {
342
342
  const out = runMatch(f, this.error);
343
343
  if (isDefectMarker(out)) return defectRes(out.cause);
344
- if (!isResult(out)) return nonResultCallbackDefect();
344
+ if (!isResult(out)) return nonResultCallbackDefect(out);
345
345
  return out;
346
346
  } catch (cause) {
347
347
  return defectRes(cause);
@@ -362,6 +362,7 @@ var Res = class {
362
362
  try {
363
363
  const out = runMatch(f, this.error);
364
364
  if (isDefectMarker(out)) return observerThrowToDefect(out.cause, this.error);
365
+ silenceIfThenable(out);
365
366
  return this;
366
367
  } catch (cause) {
367
368
  return observerThrowToDefect(cause, this.error);
@@ -372,7 +373,7 @@ var Res = class {
372
373
  try {
373
374
  const r = runMatch(f, this.error);
374
375
  if (isDefectMarker(r)) return observerThrowToDefect(r.cause, this.error);
375
- if (!isResult(r)) return nonResultCallbackDefect();
376
+ if (!isResult(r)) return nonResultCallbackDefect(r);
376
377
  return r.tag === "Ok" ? this : passThrough(r);
377
378
  } catch (cause) {
378
379
  return observerThrowToDefect(cause, this.error);
@@ -382,7 +383,7 @@ var Res = class {
382
383
  if (this.tag !== "Defect") return this;
383
384
  try {
384
385
  const r = f(this.cause);
385
- return isResult(r) ? r : nonResultCallbackDefect();
386
+ return isResult(r) ? r : nonResultCallbackDefect(r);
386
387
  } catch (cause) {
387
388
  return defectRes(cause);
388
389
  }
@@ -390,7 +391,7 @@ var Res = class {
390
391
  tapDefect(f) {
391
392
  if (this.tag !== "Defect") return this;
392
393
  try {
393
- f(this.cause);
394
+ silenceIfThenable(f(this.cause));
394
395
  return this;
395
396
  } catch (cause) {
396
397
  return observerThrowToDefect(cause, this.cause);
@@ -399,7 +400,7 @@ var Res = class {
399
400
  tapFailure(f) {
400
401
  if (this.tag === "Ok") return this;
401
402
  try {
402
- f(this);
403
+ silenceIfThenable(f(this));
403
404
  return this;
404
405
  } catch (cause) {
405
406
  return observerThrowToDefect(cause, this.tag === "Err" ? this.error : this.cause);
@@ -564,6 +565,30 @@ function passThrough(self) {
564
565
  return self;
565
566
  }
566
567
  /**
568
+ * Adopt-and-silence a thenable a combinator is about to **discard**.
569
+ *
570
+ * @remarks
571
+ * The observers (`tap`, `tapErrCases`, `tapDefect`, `tapFailure`) throw their
572
+ * callback's return value away, and the `Result`-returning combinators reject a
573
+ * non-`Result` one. Either way, a thenable that slipped past `NotThenable` (a
574
+ * cast, a raw-JS caller) is dropped while still in flight — and if it later
575
+ * rejects, nothing is holding it, so the rejection floats unhandled and takes
576
+ * the process down on Node by default. Worse for an observer: its whole job is
577
+ * to make a failure visible, and this is the one path where the failure is
578
+ * invisible.
579
+ *
580
+ * Adopting it costs one microtask and makes the rejection a no-op. The
581
+ * boundaries already do exactly this for a thenable `qualify` and a thenable
582
+ * `fn` (see `interop.ts`); this is the same net on the combinator side.
583
+ *
584
+ * @internal
585
+ */
586
+ function silenceIfThenable(value) {
587
+ try {
588
+ if ((typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function") Promise.resolve(value).then(void 0, () => void 0);
589
+ } catch {}
590
+ }
591
+ /**
567
592
  * The Defect minted when a callback constrained to return a `Result` returns
568
593
  * something else — reachable only from untyped/cast callers (in typed code the
569
594
  * constraint is a compile error). The combinator-side sibling of the
@@ -573,7 +598,8 @@ function passThrough(self) {
573
598
  *
574
599
  * @internal
575
600
  */
576
- function nonResultCallbackDefect() {
601
+ function nonResultCallbackDefect(returned) {
602
+ silenceIfThenable(returned);
577
603
  return defectRes(/* @__PURE__ */ new TypeError("unthrown: a combinator callback returned a non-Result value"));
578
604
  }
579
605
  /**
@@ -658,7 +684,7 @@ var AsyncRes = class AsyncRes {
658
684
  if (r.tag !== "Ok") return passThrough(r);
659
685
  try {
660
686
  const inner = await f(r.value);
661
- return isResult(inner) ? inner : nonResultCallbackDefect();
687
+ return isResult(inner) ? inner : nonResultCallbackDefect(inner);
662
688
  } catch (cause) {
663
689
  return defectRes(cause);
664
690
  }
@@ -668,7 +694,7 @@ var AsyncRes = class AsyncRes {
668
694
  return new AsyncRes(this.#promise.then((r) => {
669
695
  if (r.tag !== "Ok") return r;
670
696
  try {
671
- f(r.value);
697
+ silenceIfThenable(f(r.value));
672
698
  return r;
673
699
  } catch (cause) {
674
700
  return defectRes(cause);
@@ -680,7 +706,7 @@ var AsyncRes = class AsyncRes {
680
706
  if (r.tag !== "Ok") return passThrough(r);
681
707
  try {
682
708
  const inner = await f(r.value);
683
- if (!isResult(inner)) return nonResultCallbackDefect();
709
+ if (!isResult(inner)) return nonResultCallbackDefect(inner);
684
710
  return inner.tag === "Ok" ? r : passThrough(inner);
685
711
  } catch (cause) {
686
712
  return defectRes(cause);
@@ -692,7 +718,7 @@ var AsyncRes = class AsyncRes {
692
718
  if (r.tag !== "Ok") return passThrough(r);
693
719
  try {
694
720
  const inner = await f(r.value);
695
- if (!isResult(inner)) return nonResultCallbackDefect();
721
+ if (!isResult(inner)) return nonResultCallbackDefect(inner);
696
722
  if (inner.tag !== "Ok") return passThrough(inner);
697
723
  return okRes({
698
724
  ...scopeOf(r.value),
@@ -751,7 +777,7 @@ var AsyncRes = class AsyncRes {
751
777
  const out = runMatch(f, r.error);
752
778
  if (isDefectMarker(out)) return defectRes(out.cause);
753
779
  const inner = await out;
754
- if (!isResult(inner)) return nonResultCallbackDefect();
780
+ if (!isResult(inner)) return nonResultCallbackDefect(inner);
755
781
  return inner;
756
782
  } catch (cause) {
757
783
  return defectRes(cause);
@@ -776,6 +802,7 @@ var AsyncRes = class AsyncRes {
776
802
  try {
777
803
  const out = runMatch(f, r.error);
778
804
  if (isDefectMarker(out)) return observerThrowToDefect(out.cause, r.error);
805
+ silenceIfThenable(out);
779
806
  return r;
780
807
  } catch (cause) {
781
808
  return observerThrowToDefect(cause, r.error);
@@ -789,7 +816,7 @@ var AsyncRes = class AsyncRes {
789
816
  const out = runMatch(f, r.error);
790
817
  if (isDefectMarker(out)) return observerThrowToDefect(out.cause, r.error);
791
818
  const inner = await out;
792
- if (!isResult(inner)) return nonResultCallbackDefect();
819
+ if (!isResult(inner)) return nonResultCallbackDefect(inner);
793
820
  return inner.tag === "Ok" ? passThrough(r) : passThrough(inner);
794
821
  } catch (cause) {
795
822
  return observerThrowToDefect(cause, r.error);
@@ -801,7 +828,7 @@ var AsyncRes = class AsyncRes {
801
828
  if (r.tag !== "Defect") return r;
802
829
  try {
803
830
  const inner = await f(r.cause);
804
- return isResult(inner) ? inner : nonResultCallbackDefect();
831
+ return isResult(inner) ? inner : nonResultCallbackDefect(inner);
805
832
  } catch (cause) {
806
833
  return defectRes(cause);
807
834
  }
@@ -811,7 +838,7 @@ var AsyncRes = class AsyncRes {
811
838
  return new AsyncRes(this.#promise.then((r) => {
812
839
  if (r.tag !== "Defect") return r;
813
840
  try {
814
- f(r.cause);
841
+ silenceIfThenable(f(r.cause));
815
842
  return r;
816
843
  } catch (cause) {
817
844
  return observerThrowToDefect(cause, r.cause);
@@ -822,7 +849,7 @@ var AsyncRes = class AsyncRes {
822
849
  return new AsyncRes(this.#promise.then((r) => {
823
850
  if (r.tag === "Ok") return r;
824
851
  try {
825
- f(r);
852
+ silenceIfThenable(f(r));
826
853
  return r;
827
854
  } catch (cause) {
828
855
  return observerThrowToDefect(cause, r.tag === "Err" ? r.error : r.cause);
@@ -1090,6 +1117,11 @@ function fromNullable(value, onAbsent) {
1090
1117
  * — and a thenable slipped past the types at runtime becomes a `Defect` (never
1091
1118
  * an `Err(Promise)`), its orphaned rejection silenced.
1092
1119
  *
1120
+ * `fn` is **synchronous** too. An `async` `fn` rejects *after* this boundary has
1121
+ * already returned, so its rejection could never reach `qualify`: it becomes a
1122
+ * `Defect` (never `Ok(<Promise>)`) and the orphaned rejection is silenced rather
1123
+ * than left to float. Reach for {@link fromPromise} to wrap async work.
1124
+ *
1093
1125
  * The modeled error type is `Exclude<R, Defect>` — the `Defect` arm of
1094
1126
  * `qualify`'s return is **subtracted** from `E`, never inferred into it. So a
1095
1127
  * `qualify` that returns *only* `defect(cause)` yields `E = never` (a Defect is
@@ -1126,7 +1158,8 @@ function fromThrowable(fn, qualify) {
1126
1158
  const triage = qualify;
1127
1159
  return (...args) => {
1128
1160
  try {
1129
- return Ok(fn(...args));
1161
+ const value = fn(...args);
1162
+ return isThenable(value) ? thenableReturnDefect(value) : Ok(value);
1130
1163
  } catch (cause) {
1131
1164
  return qualifyToResult(cause, triage);
1132
1165
  }
@@ -1143,6 +1176,10 @@ function fromThrowable(fn, qualify) {
1143
1176
  * `qualify`. When some throws *are* anticipated, reach for
1144
1177
  * {@link fromThrowable} and triage them.
1145
1178
  *
1179
+ * `fn` is **synchronous**: an `async` `fn` becomes a `Defect` (never
1180
+ * `Ok(<Promise>)`), with its orphaned rejection silenced rather than left to
1181
+ * float. Reach for {@link fromSafePromise} to wrap async work.
1182
+ *
1146
1183
  * @typeParam A - the wrapped function's argument tuple.
1147
1184
  * @typeParam T - the wrapped function's return type.
1148
1185
  * @param fn - the throwing function to wrap.
@@ -1164,7 +1201,8 @@ function fromThrowable(fn, qualify) {
1164
1201
  function fromSafeThrowable(fn) {
1165
1202
  return (...args) => {
1166
1203
  try {
1167
- return Ok(fn(...args));
1204
+ const value = fn(...args);
1205
+ return isThenable(value) ? thenableReturnDefect(value) : Ok(value);
1168
1206
  } catch (cause) {
1169
1207
  return defectRes(cause);
1170
1208
  }
@@ -1260,7 +1298,33 @@ function qualifyToResult(cause, qualify) {
1260
1298
  }
1261
1299
  }
1262
1300
  /**
1263
- * Runtime thenable probe for the belt-and-braces guard above. Called inside the
1301
+ * The Defect minted when a **synchronous** boundary's `fn` returns a thenable —
1302
+ * i.e. an `async` function was handed to {@link fromThrowable} /
1303
+ * {@link fromSafeThrowable}.
1304
+ *
1305
+ * @remarks
1306
+ * This is the sibling of the thenable-`qualify` net in {@link qualifyToResult},
1307
+ * and it closes a strictly worse hole. A synchronous boundary only ever sees a
1308
+ * synchronous `throw`, so an async `fn`'s rejection never reaches `qualify` at
1309
+ * all: it would sit inside `Ok(<Promise>)`, un-triaged, and then float as an
1310
+ * unhandled rejection — which terminates the process on Node by default.
1311
+ *
1312
+ * Unlike the combinator callbacks, this cannot be banned at compile time
1313
+ * without collateral damage: `T & NotThenable<T>` on `fn`'s return makes a
1314
+ * **generic** function unassignable, so `fromSafeThrowable(structuredClone)`
1315
+ * stops compiling and `T` collapses to `unknown`. (The phantom rest-tuple guard
1316
+ * `fromPromise` uses fares worse.) So the ban is enforced here, at runtime,
1317
+ * where it costs nothing: a Defect, plus adopt-and-silence so the orphaned
1318
+ * rejection cannot float.
1319
+ *
1320
+ * @internal
1321
+ */
1322
+ function thenableReturnDefect(value) {
1323
+ Promise.resolve(value).then(void 0, () => void 0);
1324
+ 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."));
1325
+ }
1326
+ /**
1327
+ * Runtime thenable probe for the belt-and-braces guards above. Called inside the
1264
1328
  * caller's `try`, so even a hostile `.then` getter lands on the Defect path.
1265
1329
  *
1266
1330
  * @internal
package/dist/index.d.cts CHANGED
@@ -296,10 +296,17 @@ type Bound<T, K extends string, U> = Prettify<Omit<T, K> & { readonly [P in K]:
296
296
  * would escape the pipeline as an unhandled rejection instead of a `Defect`.
297
297
  * Lift async work with {@link fromPromise} and compose it with `flatMap`.
298
298
  *
299
+ * Spelled with `Extract`, not `[R] extends [PromiseLike<…>]`, so the ban also
300
+ * fires when only SOME arms of a union return are thenable — a *sometimes*-async
301
+ * callback (`flag ? 1 : work()`) is still an unawaited effect whose rejection
302
+ * the pipeline never sees. The tuple-wrapped form is false for a partial union
303
+ * and let exactly that through. This is the same reasoning `fromPromise`'s
304
+ * async-qualify guard already used.
305
+ *
299
306
  * @typeParam R - the callback's inferred return type.
300
307
  * @category Types
301
308
  */
302
- type NotThenable<R> = [R] extends [PromiseLike<unknown>] ? "unthrown: combinator callbacks are synchronous — lift async work with fromPromise and compose with flatMap" : unknown;
309
+ type NotThenable<R> = [Extract<R, PromiseLike<unknown>>] extends [never] ? unknown : "unthrown: combinator callbacks are synchronous — lift async work with fromPromise and compose with flatMap";
303
310
  /**
304
311
  * The built-in match builder over an error union `E`, as produced by
305
312
  * `match(error)`. This is what an error combinator's callback receives — chain
@@ -1558,6 +1565,11 @@ declare function fromNullable<T, E>(value: T | null | undefined, onAbsent: () =>
1558
1565
  * — and a thenable slipped past the types at runtime becomes a `Defect` (never
1559
1566
  * an `Err(Promise)`), its orphaned rejection silenced.
1560
1567
  *
1568
+ * `fn` is **synchronous** too. An `async` `fn` rejects *after* this boundary has
1569
+ * already returned, so its rejection could never reach `qualify`: it becomes a
1570
+ * `Defect` (never `Ok(<Promise>)`) and the orphaned rejection is silenced rather
1571
+ * than left to float. Reach for {@link fromPromise} to wrap async work.
1572
+ *
1561
1573
  * The modeled error type is `Exclude<R, Defect>` — the `Defect` arm of
1562
1574
  * `qualify`'s return is **subtracted** from `E`, never inferred into it. So a
1563
1575
  * `qualify` that returns *only* `defect(cause)` yields `E = never` (a Defect is
@@ -1602,6 +1614,10 @@ declare function fromThrowable<A extends unknown[], T, R>(fn: (...args: A) => T,
1602
1614
  * `qualify`. When some throws *are* anticipated, reach for
1603
1615
  * {@link fromThrowable} and triage them.
1604
1616
  *
1617
+ * `fn` is **synchronous**: an `async` `fn` becomes a `Defect` (never
1618
+ * `Ok(<Promise>)`), with its orphaned rejection silenced rather than left to
1619
+ * float. Reach for {@link fromSafePromise} to wrap async work.
1620
+ *
1605
1621
  * @typeParam A - the wrapped function's argument tuple.
1606
1622
  * @typeParam T - the wrapped function's return type.
1607
1623
  * @param fn - the throwing function to wrap.
package/dist/index.d.mts CHANGED
@@ -296,10 +296,17 @@ type Bound<T, K extends string, U> = Prettify<Omit<T, K> & { readonly [P in K]:
296
296
  * would escape the pipeline as an unhandled rejection instead of a `Defect`.
297
297
  * Lift async work with {@link fromPromise} and compose it with `flatMap`.
298
298
  *
299
+ * Spelled with `Extract`, not `[R] extends [PromiseLike<…>]`, so the ban also
300
+ * fires when only SOME arms of a union return are thenable — a *sometimes*-async
301
+ * callback (`flag ? 1 : work()`) is still an unawaited effect whose rejection
302
+ * the pipeline never sees. The tuple-wrapped form is false for a partial union
303
+ * and let exactly that through. This is the same reasoning `fromPromise`'s
304
+ * async-qualify guard already used.
305
+ *
299
306
  * @typeParam R - the callback's inferred return type.
300
307
  * @category Types
301
308
  */
302
- type NotThenable<R> = [R] extends [PromiseLike<unknown>] ? "unthrown: combinator callbacks are synchronous — lift async work with fromPromise and compose with flatMap" : unknown;
309
+ type NotThenable<R> = [Extract<R, PromiseLike<unknown>>] extends [never] ? unknown : "unthrown: combinator callbacks are synchronous — lift async work with fromPromise and compose with flatMap";
303
310
  /**
304
311
  * The built-in match builder over an error union `E`, as produced by
305
312
  * `match(error)`. This is what an error combinator's callback receives — chain
@@ -1558,6 +1565,11 @@ declare function fromNullable<T, E>(value: T | null | undefined, onAbsent: () =>
1558
1565
  * — and a thenable slipped past the types at runtime becomes a `Defect` (never
1559
1566
  * an `Err(Promise)`), its orphaned rejection silenced.
1560
1567
  *
1568
+ * `fn` is **synchronous** too. An `async` `fn` rejects *after* this boundary has
1569
+ * already returned, so its rejection could never reach `qualify`: it becomes a
1570
+ * `Defect` (never `Ok(<Promise>)`) and the orphaned rejection is silenced rather
1571
+ * than left to float. Reach for {@link fromPromise} to wrap async work.
1572
+ *
1561
1573
  * The modeled error type is `Exclude<R, Defect>` — the `Defect` arm of
1562
1574
  * `qualify`'s return is **subtracted** from `E`, never inferred into it. So a
1563
1575
  * `qualify` that returns *only* `defect(cause)` yields `E = never` (a Defect is
@@ -1602,6 +1614,10 @@ declare function fromThrowable<A extends unknown[], T, R>(fn: (...args: A) => T,
1602
1614
  * `qualify`. When some throws *are* anticipated, reach for
1603
1615
  * {@link fromThrowable} and triage them.
1604
1616
  *
1617
+ * `fn` is **synchronous**: an `async` `fn` becomes a `Defect` (never
1618
+ * `Ok(<Promise>)`), with its orphaned rejection silenced rather than left to
1619
+ * float. Reach for {@link fromSafePromise} to wrap async work.
1620
+ *
1605
1621
  * @typeParam A - the wrapped function's argument tuple.
1606
1622
  * @typeParam T - the wrapped function's return type.
1607
1623
  * @param fn - the throwing function to wrap.
package/dist/index.mjs CHANGED
@@ -24,7 +24,7 @@ var NonExhaustiveError = class extends Error {
24
24
  constructor(input) {
25
25
  let printed;
26
26
  try {
27
- printed = JSON.stringify(input);
27
+ printed = JSON.stringify(input) ?? String(input);
28
28
  } catch {
29
29
  printed = String(input);
30
30
  }
@@ -260,7 +260,7 @@ var Res = class {
260
260
  if (this.tag !== "Ok") return passThrough(this);
261
261
  try {
262
262
  const r = f(this.value);
263
- return isResult(r) ? r : nonResultCallbackDefect();
263
+ return isResult(r) ? r : nonResultCallbackDefect(r);
264
264
  } catch (cause) {
265
265
  return defectRes(cause);
266
266
  }
@@ -268,7 +268,7 @@ var Res = class {
268
268
  tap(f) {
269
269
  if (this.tag !== "Ok") return this;
270
270
  try {
271
- f(this.value);
271
+ silenceIfThenable(f(this.value));
272
272
  return this;
273
273
  } catch (cause) {
274
274
  return defectRes(cause);
@@ -278,7 +278,7 @@ var Res = class {
278
278
  if (this.tag !== "Ok") return this;
279
279
  try {
280
280
  const r = f(this.value);
281
- if (!isResult(r)) return nonResultCallbackDefect();
281
+ if (!isResult(r)) return nonResultCallbackDefect(r);
282
282
  return r.tag === "Ok" ? this : passThrough(r);
283
283
  } catch (cause) {
284
284
  return defectRes(cause);
@@ -288,7 +288,7 @@ var Res = class {
288
288
  if (this.tag !== "Ok") return passThrough(this);
289
289
  try {
290
290
  const r = f(this.value);
291
- if (!isResult(r)) return nonResultCallbackDefect();
291
+ if (!isResult(r)) return nonResultCallbackDefect(r);
292
292
  if (r.tag !== "Ok") return passThrough(r);
293
293
  return okRes({
294
294
  ...scopeOf(this.value),
@@ -340,7 +340,7 @@ var Res = class {
340
340
  try {
341
341
  const out = runMatch(f, this.error);
342
342
  if (isDefectMarker(out)) return defectRes(out.cause);
343
- if (!isResult(out)) return nonResultCallbackDefect();
343
+ if (!isResult(out)) return nonResultCallbackDefect(out);
344
344
  return out;
345
345
  } catch (cause) {
346
346
  return defectRes(cause);
@@ -361,6 +361,7 @@ var Res = class {
361
361
  try {
362
362
  const out = runMatch(f, this.error);
363
363
  if (isDefectMarker(out)) return observerThrowToDefect(out.cause, this.error);
364
+ silenceIfThenable(out);
364
365
  return this;
365
366
  } catch (cause) {
366
367
  return observerThrowToDefect(cause, this.error);
@@ -371,7 +372,7 @@ var Res = class {
371
372
  try {
372
373
  const r = runMatch(f, this.error);
373
374
  if (isDefectMarker(r)) return observerThrowToDefect(r.cause, this.error);
374
- if (!isResult(r)) return nonResultCallbackDefect();
375
+ if (!isResult(r)) return nonResultCallbackDefect(r);
375
376
  return r.tag === "Ok" ? this : passThrough(r);
376
377
  } catch (cause) {
377
378
  return observerThrowToDefect(cause, this.error);
@@ -381,7 +382,7 @@ var Res = class {
381
382
  if (this.tag !== "Defect") return this;
382
383
  try {
383
384
  const r = f(this.cause);
384
- return isResult(r) ? r : nonResultCallbackDefect();
385
+ return isResult(r) ? r : nonResultCallbackDefect(r);
385
386
  } catch (cause) {
386
387
  return defectRes(cause);
387
388
  }
@@ -389,7 +390,7 @@ var Res = class {
389
390
  tapDefect(f) {
390
391
  if (this.tag !== "Defect") return this;
391
392
  try {
392
- f(this.cause);
393
+ silenceIfThenable(f(this.cause));
393
394
  return this;
394
395
  } catch (cause) {
395
396
  return observerThrowToDefect(cause, this.cause);
@@ -398,7 +399,7 @@ var Res = class {
398
399
  tapFailure(f) {
399
400
  if (this.tag === "Ok") return this;
400
401
  try {
401
- f(this);
402
+ silenceIfThenable(f(this));
402
403
  return this;
403
404
  } catch (cause) {
404
405
  return observerThrowToDefect(cause, this.tag === "Err" ? this.error : this.cause);
@@ -563,6 +564,30 @@ function passThrough(self) {
563
564
  return self;
564
565
  }
565
566
  /**
567
+ * Adopt-and-silence a thenable a combinator is about to **discard**.
568
+ *
569
+ * @remarks
570
+ * The observers (`tap`, `tapErrCases`, `tapDefect`, `tapFailure`) throw their
571
+ * callback's return value away, and the `Result`-returning combinators reject a
572
+ * non-`Result` one. Either way, a thenable that slipped past `NotThenable` (a
573
+ * cast, a raw-JS caller) is dropped while still in flight — and if it later
574
+ * rejects, nothing is holding it, so the rejection floats unhandled and takes
575
+ * the process down on Node by default. Worse for an observer: its whole job is
576
+ * to make a failure visible, and this is the one path where the failure is
577
+ * invisible.
578
+ *
579
+ * Adopting it costs one microtask and makes the rejection a no-op. The
580
+ * boundaries already do exactly this for a thenable `qualify` and a thenable
581
+ * `fn` (see `interop.ts`); this is the same net on the combinator side.
582
+ *
583
+ * @internal
584
+ */
585
+ function silenceIfThenable(value) {
586
+ try {
587
+ if ((typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function") Promise.resolve(value).then(void 0, () => void 0);
588
+ } catch {}
589
+ }
590
+ /**
566
591
  * The Defect minted when a callback constrained to return a `Result` returns
567
592
  * something else — reachable only from untyped/cast callers (in typed code the
568
593
  * constraint is a compile error). The combinator-side sibling of the
@@ -572,7 +597,8 @@ function passThrough(self) {
572
597
  *
573
598
  * @internal
574
599
  */
575
- function nonResultCallbackDefect() {
600
+ function nonResultCallbackDefect(returned) {
601
+ silenceIfThenable(returned);
576
602
  return defectRes(/* @__PURE__ */ new TypeError("unthrown: a combinator callback returned a non-Result value"));
577
603
  }
578
604
  /**
@@ -657,7 +683,7 @@ var AsyncRes = class AsyncRes {
657
683
  if (r.tag !== "Ok") return passThrough(r);
658
684
  try {
659
685
  const inner = await f(r.value);
660
- return isResult(inner) ? inner : nonResultCallbackDefect();
686
+ return isResult(inner) ? inner : nonResultCallbackDefect(inner);
661
687
  } catch (cause) {
662
688
  return defectRes(cause);
663
689
  }
@@ -667,7 +693,7 @@ var AsyncRes = class AsyncRes {
667
693
  return new AsyncRes(this.#promise.then((r) => {
668
694
  if (r.tag !== "Ok") return r;
669
695
  try {
670
- f(r.value);
696
+ silenceIfThenable(f(r.value));
671
697
  return r;
672
698
  } catch (cause) {
673
699
  return defectRes(cause);
@@ -679,7 +705,7 @@ var AsyncRes = class AsyncRes {
679
705
  if (r.tag !== "Ok") return passThrough(r);
680
706
  try {
681
707
  const inner = await f(r.value);
682
- if (!isResult(inner)) return nonResultCallbackDefect();
708
+ if (!isResult(inner)) return nonResultCallbackDefect(inner);
683
709
  return inner.tag === "Ok" ? r : passThrough(inner);
684
710
  } catch (cause) {
685
711
  return defectRes(cause);
@@ -691,7 +717,7 @@ var AsyncRes = class AsyncRes {
691
717
  if (r.tag !== "Ok") return passThrough(r);
692
718
  try {
693
719
  const inner = await f(r.value);
694
- if (!isResult(inner)) return nonResultCallbackDefect();
720
+ if (!isResult(inner)) return nonResultCallbackDefect(inner);
695
721
  if (inner.tag !== "Ok") return passThrough(inner);
696
722
  return okRes({
697
723
  ...scopeOf(r.value),
@@ -750,7 +776,7 @@ var AsyncRes = class AsyncRes {
750
776
  const out = runMatch(f, r.error);
751
777
  if (isDefectMarker(out)) return defectRes(out.cause);
752
778
  const inner = await out;
753
- if (!isResult(inner)) return nonResultCallbackDefect();
779
+ if (!isResult(inner)) return nonResultCallbackDefect(inner);
754
780
  return inner;
755
781
  } catch (cause) {
756
782
  return defectRes(cause);
@@ -775,6 +801,7 @@ var AsyncRes = class AsyncRes {
775
801
  try {
776
802
  const out = runMatch(f, r.error);
777
803
  if (isDefectMarker(out)) return observerThrowToDefect(out.cause, r.error);
804
+ silenceIfThenable(out);
778
805
  return r;
779
806
  } catch (cause) {
780
807
  return observerThrowToDefect(cause, r.error);
@@ -788,7 +815,7 @@ var AsyncRes = class AsyncRes {
788
815
  const out = runMatch(f, r.error);
789
816
  if (isDefectMarker(out)) return observerThrowToDefect(out.cause, r.error);
790
817
  const inner = await out;
791
- if (!isResult(inner)) return nonResultCallbackDefect();
818
+ if (!isResult(inner)) return nonResultCallbackDefect(inner);
792
819
  return inner.tag === "Ok" ? passThrough(r) : passThrough(inner);
793
820
  } catch (cause) {
794
821
  return observerThrowToDefect(cause, r.error);
@@ -800,7 +827,7 @@ var AsyncRes = class AsyncRes {
800
827
  if (r.tag !== "Defect") return r;
801
828
  try {
802
829
  const inner = await f(r.cause);
803
- return isResult(inner) ? inner : nonResultCallbackDefect();
830
+ return isResult(inner) ? inner : nonResultCallbackDefect(inner);
804
831
  } catch (cause) {
805
832
  return defectRes(cause);
806
833
  }
@@ -810,7 +837,7 @@ var AsyncRes = class AsyncRes {
810
837
  return new AsyncRes(this.#promise.then((r) => {
811
838
  if (r.tag !== "Defect") return r;
812
839
  try {
813
- f(r.cause);
840
+ silenceIfThenable(f(r.cause));
814
841
  return r;
815
842
  } catch (cause) {
816
843
  return observerThrowToDefect(cause, r.cause);
@@ -821,7 +848,7 @@ var AsyncRes = class AsyncRes {
821
848
  return new AsyncRes(this.#promise.then((r) => {
822
849
  if (r.tag === "Ok") return r;
823
850
  try {
824
- f(r);
851
+ silenceIfThenable(f(r));
825
852
  return r;
826
853
  } catch (cause) {
827
854
  return observerThrowToDefect(cause, r.tag === "Err" ? r.error : r.cause);
@@ -1089,6 +1116,11 @@ function fromNullable(value, onAbsent) {
1089
1116
  * — and a thenable slipped past the types at runtime becomes a `Defect` (never
1090
1117
  * an `Err(Promise)`), its orphaned rejection silenced.
1091
1118
  *
1119
+ * `fn` is **synchronous** too. An `async` `fn` rejects *after* this boundary has
1120
+ * already returned, so its rejection could never reach `qualify`: it becomes a
1121
+ * `Defect` (never `Ok(<Promise>)`) and the orphaned rejection is silenced rather
1122
+ * than left to float. Reach for {@link fromPromise} to wrap async work.
1123
+ *
1092
1124
  * The modeled error type is `Exclude<R, Defect>` — the `Defect` arm of
1093
1125
  * `qualify`'s return is **subtracted** from `E`, never inferred into it. So a
1094
1126
  * `qualify` that returns *only* `defect(cause)` yields `E = never` (a Defect is
@@ -1125,7 +1157,8 @@ function fromThrowable(fn, qualify) {
1125
1157
  const triage = qualify;
1126
1158
  return (...args) => {
1127
1159
  try {
1128
- return Ok(fn(...args));
1160
+ const value = fn(...args);
1161
+ return isThenable(value) ? thenableReturnDefect(value) : Ok(value);
1129
1162
  } catch (cause) {
1130
1163
  return qualifyToResult(cause, triage);
1131
1164
  }
@@ -1142,6 +1175,10 @@ function fromThrowable(fn, qualify) {
1142
1175
  * `qualify`. When some throws *are* anticipated, reach for
1143
1176
  * {@link fromThrowable} and triage them.
1144
1177
  *
1178
+ * `fn` is **synchronous**: an `async` `fn` becomes a `Defect` (never
1179
+ * `Ok(<Promise>)`), with its orphaned rejection silenced rather than left to
1180
+ * float. Reach for {@link fromSafePromise} to wrap async work.
1181
+ *
1145
1182
  * @typeParam A - the wrapped function's argument tuple.
1146
1183
  * @typeParam T - the wrapped function's return type.
1147
1184
  * @param fn - the throwing function to wrap.
@@ -1163,7 +1200,8 @@ function fromThrowable(fn, qualify) {
1163
1200
  function fromSafeThrowable(fn) {
1164
1201
  return (...args) => {
1165
1202
  try {
1166
- return Ok(fn(...args));
1203
+ const value = fn(...args);
1204
+ return isThenable(value) ? thenableReturnDefect(value) : Ok(value);
1167
1205
  } catch (cause) {
1168
1206
  return defectRes(cause);
1169
1207
  }
@@ -1259,7 +1297,33 @@ function qualifyToResult(cause, qualify) {
1259
1297
  }
1260
1298
  }
1261
1299
  /**
1262
- * Runtime thenable probe for the belt-and-braces guard above. Called inside the
1300
+ * The Defect minted when a **synchronous** boundary's `fn` returns a thenable —
1301
+ * i.e. an `async` function was handed to {@link fromThrowable} /
1302
+ * {@link fromSafeThrowable}.
1303
+ *
1304
+ * @remarks
1305
+ * This is the sibling of the thenable-`qualify` net in {@link qualifyToResult},
1306
+ * and it closes a strictly worse hole. A synchronous boundary only ever sees a
1307
+ * synchronous `throw`, so an async `fn`'s rejection never reaches `qualify` at
1308
+ * all: it would sit inside `Ok(<Promise>)`, un-triaged, and then float as an
1309
+ * unhandled rejection — which terminates the process on Node by default.
1310
+ *
1311
+ * Unlike the combinator callbacks, this cannot be banned at compile time
1312
+ * without collateral damage: `T & NotThenable<T>` on `fn`'s return makes a
1313
+ * **generic** function unassignable, so `fromSafeThrowable(structuredClone)`
1314
+ * stops compiling and `T` collapses to `unknown`. (The phantom rest-tuple guard
1315
+ * `fromPromise` uses fares worse.) So the ban is enforced here, at runtime,
1316
+ * where it costs nothing: a Defect, plus adopt-and-silence so the orphaned
1317
+ * rejection cannot float.
1318
+ *
1319
+ * @internal
1320
+ */
1321
+ function thenableReturnDefect(value) {
1322
+ Promise.resolve(value).then(void 0, () => void 0);
1323
+ 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."));
1324
+ }
1325
+ /**
1326
+ * Runtime thenable probe for the belt-and-braces guards above. Called inside the
1263
1327
  * caller's `try`, so even a hostile `.then` getter lands on the Defect path.
1264
1328
  *
1265
1329
  * @internal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unthrown",
3
- "version": "5.0.0",
3
+ "version": "5.1.0",
4
4
  "description": "Explicit errors as values, with a separate defect (panic) channel",
5
5
  "keywords": [
6
6
  "defect",