Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 0 additions & 27 deletions .changeset/nervous-moons-judge.md

This file was deleted.

23 changes: 0 additions & 23 deletions .changeset/olive-cars-repeat.md

This file was deleted.

26 changes: 0 additions & 26 deletions .changeset/seven-rules-no-unused-matcher.md

This file was deleted.

63 changes: 0 additions & 63 deletions .changeset/tall-spoons-listen.md

This file was deleted.

70 changes: 0 additions & 70 deletions .changeset/wild-moons-smoke.md

This file was deleted.

9 changes: 9 additions & 0 deletions packages/boxed/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# @unthrown/boxed

## 5.1.0

### Patch Changes

- Updated dependencies [625f985]
- Updated dependencies [1af785f]
- Updated dependencies [5ead919]
- unthrown@5.1.0

## 5.0.0

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/boxed/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@unthrown/boxed",
"version": "5.0.0",
"version": "5.1.0",
"description": "Boxed interop for unthrown",
"keywords": [
"boxed",
Expand Down
110 changes: 110 additions & 0 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,115 @@
# unthrown

## 5.1.0

### Minor Changes

- 5ead919: Close a boundary hole where an `async` function's rejection escaped
qualification, stop `toBeErrTagged` counting a message as payload, and name the
value in `NonExhaustiveError`.

**`fromThrowable` / `fromSafeThrowable` now reject an `async` `fn`.** These wrap a
**synchronous** function, so they only ever see a synchronous `throw`: an async
`fn` rejects long after the boundary has returned, and its rejection could never
reach `qualify`. It used to produce `Ok(<Promise>)` — un-triaged — and the
rejection then floated as an unhandled rejection, which terminates the process on
Node by default:

```ts
const f = fromThrowable(
async () => {
throw new Error("boom");
},
(cause, defect) => defect(cause),
);
f(); // before: Ok(<Promise>) + an unhandled rejection
// now: Defect(TypeError: … `fn` returned a thenable …)
```

The orphaned rejection is adopted and silenced, so it cannot float. Reach for
`fromPromise` / `fromSafePromise` for async work.

This is caught at runtime rather than by the type system — the one thenable ban
in the library that is not a compile error. Putting `NotThenable` on `fn`'s return
also makes **generic** functions unassignable, so `fromSafeThrowable(structuredClone)`
would stop compiling with `T` collapsed to `unknown`; the phantom rest-tuple guard
`fromPromise` uses fares worse. The success type is therefore slightly over-stated
(`Result<Promise<T>, E>` is spellable but never inhabited) — the mirror of
`recoverErrCases`'s `never` under-stating the error channel.

**`toBeErrTagged`'s exact-payload form now ignores every reserved key.** The
documented way to set a `TaggedError`'s message is a subclass field —
`override message = "…"` — which lands as an own **enumerable** property, so it
leaked into the payload and failed an exact assertion on the very pattern the
library prescribes:

```ts
class HttpError extends TaggedError("HttpError")<{ status: number }> {
override message = `http ${this.status}`;
}
expect(Err(new HttpError({ status: 500 }))).toBeErrTagged("HttpError", {
status: 500,
});
// before: failed — the payload was seen as { status: 500, message: "http 500" }
// now: passes
```

The matcher now skips `_tag`, `name`, `message` and `stack` — exactly the keys
`TaggedErrorInstance` omits and the constructor types `?: never`, so none of them
can legitimately be payload. Assertions using `expect.objectContaining` are
unaffected (they were already tolerant of the extra key).

**`NonExhaustiveError` now names the value it could not match.** `JSON.stringify`
_returns_ `undefined` — it does not throw — for a function, a symbol, or
`undefined`, so the `String(input)` fallback never fired and the message read
"no pattern matched the value undefined" for exactly the rogue inputs this error
exists to describe. It now falls back correctly (`… the value function rogueFn() {}`).

### Patch Changes

- 625f985: Adopt a discarded thenable so its rejection can't float, and stop
`createResultClient`'s proxy from ever exposing a callable `then`.

**Core: the observers now adopt-and-silence a smuggled thenable.** `tap`,
`tapErrCases`, `tapDefect` and `tapFailure` throw their callback's return value
away, and the `Result`-returning combinators (`flatMap`, `flatTap`, `bind`,
`flatMapErrCases`, `flatTapErrCases`, `recoverDefect`) reject a non-`Result`
one. Either way a thenable that slipped past `NotThenable` — via a cast or a
raw-JS caller — was dropped mid-flight, so a later rejection floated unhandled
and took the process down on Node by default. Worse for an observer: its whole
job is to make a failure visible, and this was the one path where the failure
was invisible.

The boundaries already did exactly this for a thenable `qualify` and a thenable
`fn`; this is the same net on the combinator side. The observed result still
passes through unchanged — silencing does not change any outcome.

**`@unthrown/orpc`: `createResultClient` now answers `then` with `undefined`.**
The proxy's `get` trap wraps every object/function property, so on a client
whose own proxy answers _any_ key with a nested procedure, `rc.then` would be
callable and `await rc` would invoke it. oRPC's client proxy happens to guard
`then` today, but that is its invariant to change, not ours to depend on.

- 1af785f: `NotThenable` now rejects a **sometimes**-async callback, not just an always-async
one.

It was spelled `[R] extends [PromiseLike<unknown>]`, which is false for a partial
union — so a callback that returns a promise on only one branch compiled on every
guarded surface:

```ts
result.tap(() => (flag ? 1 : work())); // used to compile
```

That is still an unawaited effect whose rejection the pipeline never sees, which
is exactly what the ban exists to prevent. Spelling it with `Extract` closes it —
the same reasoning `fromPromise`'s async-qualify guard already used, and which
this type never picked up.

Affects `map`, `tap`, `let`, `tapDefect`, `tapFailure` and `ensure`'s `onFail`. An
always-async callback was already rejected and is unchanged; a purely synchronous
one is unaffected.

## 5.0.0

### Major Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "unthrown",
"version": "5.0.0",
"version": "5.1.0",
"description": "Explicit errors as values, with a separate defect (panic) channel",
"keywords": [
"defect",
Expand Down
9 changes: 9 additions & 0 deletions packages/effect/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# @unthrown/effect

## 5.1.0

### Patch Changes

- Updated dependencies [625f985]
- Updated dependencies [1af785f]
- Updated dependencies [5ead919]
- unthrown@5.1.0

## 5.0.0

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/effect/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@unthrown/effect",
"version": "5.0.0",
"version": "5.1.0",
"description": "Effect interop for unthrown",
"keywords": [
"effect",
Expand Down
9 changes: 9 additions & 0 deletions packages/neverthrow/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# @unthrown/neverthrow

## 5.1.0

### Patch Changes

- Updated dependencies [625f985]
- Updated dependencies [1af785f]
- Updated dependencies [5ead919]
- unthrown@5.1.0

## 5.0.0

### Patch Changes
Expand Down
Loading
Loading