Skip to content

feat(core)!: match the error channel exhaustively with ts-pattern - #113

Merged
btravers merged 15 commits into
mainfrom
feat/error-channel-triage
Jul 25, 2026
Merged

feat(core)!: match the error channel exhaustively with ts-pattern#113
btravers merged 15 commits into
mainfrom
feat/error-channel-triage

Conversation

@btravers

@btravers btravers commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

What

Every error is now handled explicitly, and enriching the error channel E
is a compile error at every consuming site until each new case is handled.
The error combinators — mapErr, flatMapErr, recoverErr, tapErr,
flatTapErr — receive a ts-pattern
match builder over the error (match(error)) plus the injected defect helper,
and return the un-terminated builder — the combinator calls .exhaustive()
for you:

import { P, tag } from "unthrown";

// before — a future tag lands in the fallthrough unnoticed
.mapErr((error) => {
  if (error._tag === "RecordNotFound") return new NotFoundException(id);
  throw error.cause;
})

// after — exhaustive; the type checker forces every case to be handled
.mapErr((m, defect) =>
  m
    .with(tag("RecordNotFound"), () => new NotFoundException(id))
    .with(tag("DriverError"), (e) => defect(e.cause)),
)

Because the combinator runs .exhaustive(), a missing case does not compile
there is no .exhaustive() to forget and no .otherwise() to smuggle in a
fallback. Errors-as-values only pays off if the values can't be silently
dropped; this makes sure they aren't.

Design journey (why not the record)

This PR started as a per-tag record ({ RecordNotFound: …, DriverError: … })
with a mergeTags escape hatch. Review surfaced two real limits of that shape:
it only keyed on _tag (breaking code-discriminated unions like oRPC's), and
it couldn't share one strategy across several cases. ts-pattern dissolves both —
and, crucially, keeps the forced-exhaustiveness the record was invented to
provide, because the combinator owns .exhaustive(). So the whole bespoke
machinery (ErrTriage/mergeTags/TriageReturns/TriageKeysOk/NoDefects)
is deleted in favor of the matcher.

The rules

  • Match on anything_tag, code, structural shapes, guards, and grouped
    patterns (.with(a, b, handler) — one strategy for several cases). tag("X")
    is sugar for { _tag: "X" }.
  • P._ is the deliberate catch-all — the uniform "handle everything else"
    branch that replaces the old single callback, made explicit and greppable.
  • Each branch receives the narrowed variant and the injected defect helper;
    its Defect arm is subtracted from the outgoing E (Exclude<O, Defect>, the
    boundary inference). A throwing branch also becomes a Defect (safety net).
  • Observers match exhaustively too (tapErr/flatTapErr, P._ for a
    catch-all); the error is observed and flows through unchanged.

Type-machinery notes (recorded in CLAUDE.md → Internal design)

  • ErrMatcher<E> = ReturnType<typeof match<E>>; the callback returns an
    ExhaustiveMatch<O> whose .exhaustive is typed callable only when ts-pattern
    narrowed the input to never. Outgoing E = Exclude<MatchOut<M>, Defect>.
  • ErrMatcher<E> must stay a contravariant callback parameter only.
    ts-pattern's Match is invariant in its input; unioning an M-derived output
    with the class E in a covariant return re-invaded E's variance and
    collapsed inference (combine<T,E>(rs: AsyncResult<T,E>[])unknown). That
    is why flatTapErr infers a plain E2 and returns E | E2 — verified by a
    regression assertion in types.test-d.ts.
  • runMatch(f, error) = f(match(error), defect).run(); a value slipping past the
    types throws NonExhaustiveError, which the throw-to-defect net turns into a
    Defect. AsyncRes's five methods are typed loosely (never channels) across
    the implements boundary; the public surface re-imposes precision.
  • Async-branch handling: only flatMapErr/flatTapErr await, so only they
    reject an async branch (via the builder-output constraint); mapErr/
    recoverErr/tapErr run synchronously, so an async branch is a visible
    Promise-valued result, not a rejection bypass.

@unthrown/pattern removed; ts-pattern folded into core

Core now depends on ts-pattern (small, types-heavy, and dual-copy-safe
its matcher protocol keys off a global Symbol.for), and re-exports match and
P, plus tag. The standalone @unthrown/pattern package is deleted: tag
moved into core; the P.Ok/P.Err/P.Defect sugar is dropped (match the union
structurally with ts-pattern instead — match(r).with(P.Ok(), …) still works
since Result is a discriminated union).

Also breaking

  • Deprecated error-channel aliases orElse / recover removed. The
    extractor aliases (unwrap, unwrapErr, unwrapOr, unwrapOrElse) remain.
  • AsyncOkOf / AsyncErrOf now infer through the Awaitable channel only
    (same results for ordinary AsyncResult types).

Verification

  • Full gate green: format --check, lint, typecheck, knip, test, build
    — 36/36 turbo tasks, 386 tests across the workspace (incl. prisma SQLite and
    orpc end-to-end suites). Core 236 tests, 100% line/function coverage;
    typedoc warning-free.
  • types.test-d.ts pins: forced exhaustiveness (a missing case doesn't compile),
    P._ catch-all, grouped patterns, code-discriminated and untagged unions,
    defect/throw-branch subtraction, flatMapErr/flatTapErr async-branch
    rejection, and the combine inference regression guard.
  • Docs: the "Triaging the error channel" guide leads with the
    handle-everything-explicitly / compile-error-on-enrichment guarantee; migration
    tables, orpc guide/README, core-concepts, and the CLAUDE.md spec (Thesis chore: migrate repo + docs URLs to btravstack org #5,
    invariants, internal design, monorepo layout) all updated; the pattern package
    guide/API pages removed.

…take a per-tag triage object

The error transformers no longer take a single callback: they take an
ErrTriage object — one branch per error _tag, exhaustive at compile time,
with a reserved Else branch as the explicit blanket escape hatch (and the
only form for an untagged or mixed E). Observers (tapErr, flatTapErr,
tapDefect, tapFailure) keep single callbacks: observing a union uniformly
cannot strand a future tag; only consuming one can (Thesis #5).

- outgoing types are the union of branch returns: a throwing branch
  (never) converts its tag to a Defect AND subtracts it from E
- unmodeled tag at runtime (outside the typed contract) becomes a Defect
  carrying the error, mirroring matchTags; own-property lookup only
- ResultMethods/AsyncResultMethods/Awaitable gain verified out-variance
  annotations; ErrTriage stays free of top-level conditionals so they
  verify; AsyncOkOf/AsyncErrOf now infer through the Awaitable channel
- generic-H inference (TriageReturns/TriageKeysOk) instead of a shared R,
  which TS inference fixing would break on Else passthroughs
- BREAKING: deprecated aliases orElse/recover removed (signatures broke
  anyway); unwrap-family aliases remain
- docs: new 'Triaging the error channel' guide section; orpc examples,
  migration tables, CLAUDE.md Thesis #5 + invariants + internal design
Copilot AI review requested due to automatic review settings July 24, 2026 20:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a breaking-change update to unthrown’s core API by replacing the error-transformer callback form with a per-_tag triage object (ErrTriage) for mapErr, flatMapErr, and recoverErr, ensuring compile-time exhaustiveness for tagged error unions and making blanket handling explicit via Else.

Changes:

  • Refactors mapErr/flatMapErr/recoverErr (sync + async surfaces) to accept an ErrTriage object and adds supporting type machinery (TriageReturns, TriageKeysOk, NoThenables), plus updated AsyncOkOf/AsyncErrOf inference.
  • Removes deprecated error-channel aliases orElse / recover, and updates tests and type-level assertions accordingly.
  • Updates docs, guides, and oRPC examples/migration tables to reflect triage-based error transformations, and adds a major changeset entry.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/orpc/src/server.ts Updates inline example to use mapErr({ Else: … }) triage form.
packages/orpc/src/extensions/result.ts Updates extension example to use triage form.
packages/orpc/README.md Updates README examples to use triage object (including multiline formatting).
packages/core/typedoc.json Adds triage helper types to intentionallyNotExported for docs generation.
packages/core/src/types.ts Adds ErrTriage + triage helper types; updates method surface signatures and variance annotations; updates async extractor inference.
packages/core/src/types.test-d.ts Adds/updates type-level assertions for triage exhaustiveness, Else forms, and async-branch rejection.
packages/core/src/result.spec.ts Adds runtime tests for triage dispatch semantics, Else fallback/precedence, and unhandled-tag behavior.
packages/core/src/invariants.spec.ts Updates invariants to use triage form for error transformers.
packages/core/src/index.ts Re-exports ErrTriage type from the public entry point.
packages/core/src/deprecated-aliases.spec.ts Removes tests for deleted orElse/recover aliases; keeps extractor-alias tests.
packages/core/src/core.ts Implements triage-based dispatch in Res and AsyncRes via triageHandlerFor.
packages/core/src/async-result.spec.ts Updates async runtime tests to use triage form; keeps async composition coverage.
docs/guide/the-defect-channel.md Updates examples to triage syntax for mapErr/recoverErr.
docs/guide/recipes.md Updates recipe wording + example to use triage form.
docs/guide/orpc.md Updates oRPC guide examples to triage form; updates a more complex mapping example to be per-tag.
docs/guide/migrating-from-neverthrow.md Updates migration table entries for mapErr/orElse to triage-based equivalents.
docs/guide/from-try-catch.md Updates mapping examples/table to triage-based mapErr({ Else: … }).
docs/guide/core-concepts.md Clarifies that error transformers are triage-based and updates examples.
docs/guide/choosing-a-combinator.md Adds a “Triaging the error channel” section and updates signatures/tables to reflect triage objects.
CLAUDE.md Updates the project spec (Thesis #5) and invariants/internal design notes for triage.
.changeset/triage-error-channel.md Adds a major changeset describing the breaking change and migration guidance.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/core/src/core.ts Outdated
btravers added 2 commits July 24, 2026 23:04
…rvers take the partial triage

Two refinements to the triage object, from review:

- Transformer branches (mapErr/flatMapErr/recoverErr) receive the injected
  defect helper as their second argument — the same injection qualify gets
  at a boundary, and now the sanctioned deliberate Err→Defect form
  ((e, defect) => defect(e.cause)); the outgoing types subtract the Defect
  arm (Exclude<TriageReturns<H>, Defect>, the boundary inference). A
  throwing branch remains the safety net. This keeps deliberate defects
  lint-clean under a no-throw rule and scopes minting to triage decisions,
  preserving the #77 rejection of a public constructor.
- tapErr/flatTapErr take the same triage object in its PARTIAL form
  (ErrTriagePartial: every branch optional, Else included) — an unobserved
  tag flows through, so per-tag observation needs no manual narrowing and
  uniform logging is { Else: log }. One object shape across the error
  channel: exhaustive when you consume, partial when you observe. Observer
  branches do NOT receive defect (their returns never replace the error;
  withholding the helper keeps the marker unconstructible there).
  tapDefect/tapFailure keep single callbacks (no tags to triage).
- TriageKeysOk also rejects a bare function as the handlers argument — it
  would structurally satisfy the all-optional shape and silently observe
  nothing.
The triage object is now exhaustive, full stop — no fallthrough branch
inside it. The explicit opt-out of exhaustiveness is the separate,
greppable mergeTags(fn) wrapper (a unique-symbol-branded MergedTriage the
error combinators accept in place of the per-tag object):

  .tapErr(mergeTags((err) => logger.error(err)))
  .mapErr(mergeTags((e, defect) => (isRetryable(e) ? e : defect(e))))

- Else undermined the purpose of the change: a fallthrough living inside
  the exhaustive object, silently absorbing future tags. mergeTags moves
  the uniform decision to a distinct call site that reads as an explicit
  opt-out and is auditable across a codebase. There is no
  partial-with-fallback middle ground anymore: a union that treats
  several tags identically writes each branch explicitly — a new tag is
  a compile error, never silently absorbed.
- mergeTags is the only form for an untagged or mixed E (per-tag triage
  is impossible there), and the one-line migration of old callback call
  sites: mapErr(f) -> mapErr(mergeTags(f)).
- Merged transformer handlers receive the injected defect helper
  (qualify-style triage over non-_tag unions); merged observers that
  produce a Defect are rejected at compile time (NoDefects), and a bare
  callback is rejected on all six methods (TriageKeysOk).
- The merged overloads are declared BEFORE the object overloads —
  contextual inference of the merged callback's parameter depends on the
  order.
- 'Else' is no longer a reserved key: an error tag literally named Else
  is an ordinary tag.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 22 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (7)

packages/core/src/types.ts:354

  • This section still references a missing Else branch ("no branch and no Else"). The behavior is simply "no branch" (only reachable outside the typed contract), which becomes a Defect.
   * An error whose tag has no branch and no `Else` (unreachable through
   * well-typed code) becomes a `Defect` carrying it. An async branch is
   * rejected at compile time ({@link NotThenable}).

packages/core/src/types.ts:359

  • The @param docs still mention using an Else branch for blanket handling, but uniform handling is done via mergeTags(fn) rather than a reserved object key.
   * @param handlers - the triage object: per-tag branches, each mapping its
   * error to a new one (or `Else` for the deliberate blanket case).

packages/core/src/types.ts:375

  • flatMapErr’s TSDoc repeats the idea of an Else fallthrough branch. The triage object is exhaustive; the explicit uniform form is mergeTags(fn).
   * mirror of {@link ResultMethods.flatMap | flatMap}, **triaging the error by
   * tag** ({@link ErrTriage}: exhaustive unless `Else` is present).

packages/core/src/types.ts:404

  • recoverErr’s TSDoc still says the triage is "exhaustive unless Else is present", which doesn’t match the current API (no fallthrough key; mergeTags is the opt-out).
   * Recover from an `Err` by producing a success value, emptying the error
   * channel — **triaging the error by tag** ({@link ErrTriage}: exhaustive
   * unless `Else` is present). Pairs with
   * {@link ResultMethods.recoverDefect | recoverDefect}.

packages/core/src/types.ts:434

  • tapErr’s TSDoc claims the partial triage includes an Else branch and later shows { Else: ... } as uniform observation. Uniform observation is via tapErr(mergeTags(fn)); Else is just an ordinary tag name if it exists in E.
   * Run a side effect on the error — **observed by tag** through a *partial*
   * triage object ({@link ErrTriagePartial}: every branch optional, `Else`
   * included) — and pass the `Result` through unchanged.

packages/core/src/types.ts:438

  • The uniform-observation example uses { Else: ... }, but the uniform form is mergeTags(fn) (and the per-tag partial object doesn’t have a reserved fallthrough key).
   * Runs the matching branch only on `Err`; a tag without a branch is simply
   * not observed. Uniform observation is `{ Else: (e) => log(e) }`. If a
   * branch throws, the result is a `Defect` whose cause is an `AggregateError`

packages/core/src/core.ts:507

  • triageHandlerFor can throw when handlers is null/undefined (and it’s called outside the surrounding try/catch). That would violate the library’s “throw → Defect” invariant for sync Results, and can also cause AsyncRes’s internal promise chain to reject (breaking the “AsyncResult promise never rejects” invariant). Adding a null/typeof guard here keeps invalid handler values contained as a Defect instead of escaping as a raw throw/rejection.
  if (isMergedTriage(handlers)) {
    // The explicit uniform form: one handler for every error, tagged or not.
    return handlers.handler as (error: unknown, defect?: (cause: unknown) => Defect) => unknown;
  }
  const triage = handlers as Record<string, unknown>;

Comment thread packages/core/src/types.ts Outdated
Comment thread packages/core/src/core.ts Outdated
Comment thread .changeset/triage-error-channel.md Outdated
btravers added 2 commits July 24, 2026 23:48
A transformer accepts {} only where the uncovered set is empty. New
type-level assertions in types.test-d.ts lock this against regression:

- coded.mapErr({}) on a code-discriminated (non-_tag) union — REJECTED
  (the orpc shape: no _tag keys, forced to mergeTags)
- coded.mapErr({ NOT_FOUND, FORBIDDEN }) — REJECTED (triage keys off _tag)
- none.mapErr({}) on E = never — compiles (vacuously exhaustive)
- coded.tapErr({}) — compiles as an observer, but the error type is
  UNCHANGED: observing zero cases does not consume the error

Documented as a load-bearing invariant in CLAUDE.md and a tip block in
the choosing-a-combinator guide: there is no path where a case slips past
a transformer uncovered without a compile error.
Replace the bespoke record/mergeTags triage with a ts-pattern match
builder. The error combinators (mapErr/flatMapErr/recoverErr/tapErr/
flatTapErr) now receive match(error) plus the injected defect helper and
return the un-terminated builder; the combinator calls .exhaustive()
itself, so a missing case does not compile — no .exhaustive() to forget,
no .otherwise() to smuggle in a fallback.

Why this is better than the record:
- matches ANY discriminant (_tag, code, structural, guards) and grouped
  patterns — not just _tag; fixes the two limits raised in review
- P._ is the explicit catch-all (replaces the old single callback /
  mergeTags), exhaustive and greppable
- one construct instead of {record + mergeTags}; smaller surface

Mechanics:
- ErrMatcher<E> = ReturnType<typeof match<E>>; the callback returns an
  ExhaustiveMatch<O> whose .exhaustive is callable only when ts-pattern
  narrowed the input to never. Outgoing E = Exclude<MatchOut<M>, Defect>
  (a defect branch drops its case, the boundary inference)
- runMatch(f, error) = f(match(error), defect).run(); NonExhaustiveError
  from a value that slips past the types becomes a Defect (throw-to-defect)
- flatTapErr infers a plain E2 and returns E | E2: unioning an M-derived
  output with the class E in a covariant return re-invades E's variance
  and collapses inference (combine<T,E> → unknown). ErrMatcher<E> stays a
  contravariant callback parameter only.

Core now depends on ts-pattern (small, types-heavy, dual-copy-safe via a
global Symbol.for) and re-exports match/P + tag. The @unthrown/pattern
package is removed — tag moved into core; the P.Ok/Err/Defect sugar
dropped (match the union structurally instead).

BREAKING: deprecated orElse/recover removed; unwrap-family aliases remain.
AsyncOkOf/AsyncErrOf infer through the Awaitable channel only.
@btravers btravers changed the title feat(core)!: triage the error channel — mapErr/flatMapErr/recoverErr take a per-tag triage object feat(core)!: match the error channel exhaustively with ts-pattern Jul 24, 2026
@btravers
btravers requested a review from Copilot July 24, 2026 23:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 40 changed files in this pull request and generated 2 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

Comment thread packages/core/src/types.ts
Comment thread CLAUDE.md Outdated
btravers added 4 commits July 25, 2026 01:44
The prior 5.0.7 pin (GHSA-3jxr-9vmj-r5cp) is now flagged by a new high
advisory, GHSA-mh99-v99m-4gvg (DoS via unbounded expansion length),
failing the Security Audit gate. 5.0.8 is the first release patched for
both. Add a temporary minimumReleaseAge exclusion — it was published
2026-07-23, inside the 7-day maturity cutoff.
…chers

The load-bearing invariant still described the pre-matcher `NoThenables`
union-wide check applied by `mapErr`/`recoverErr`/`tapErr` — a type that
no longer exists. Match reality (and the file's own Thesis #5): only the
awaiting `flatMapErr`/`flatTapErr` reject an async branch via their
builder-output constraint; the non-awaiting triage combinators run the
branch synchronously, so an async branch is a visible Promise value, not
a rejection bypass.
Remove `matchTags` / `TagHandlers` and upgrade `match`'s `err` handler to
the same ts-pattern exhaustive matcher the error combinators use. A per-tag
fold at the edge is now:

  result.match({
    ok, defect,
    err: (matcher) => matcher.with(tag("NotFound"), …).with(tag("Forbidden"), …),
  })

which generalises beyond `_tag` to any discriminant. The `err` handler
returns the un-terminated builder (match runs `.exhaustive()`) and receives
no `defect` helper — match is total elimination to a value. `ok`/`defect`
have independent result type params, so divergent channel shapes union
cleanly.

Also rename the matcher callback param `m` → `matcher` across every error
combinator.

Library code generic in `E` can't prove `P._` exhaustive over an unresolved
type parameter, so the interop `to*` bridges and `@unthrown/orpc`'s
handlerResult fold via the isOk/isErr/isDefect guards instead.

BREAKING CHANGE: `matchTags` and `TagHandlers` are removed; `match`'s `err`
handler now takes the exhaustive matcher instead of a blanket callback.
Show the single-wildcard-branch idiom (log/transform/recover/fold any
error) in the error-triage guide, and note that a P._ branch keeps
compiling as E is enriched — the deliberate opt-out of per-case handling.
Adds .changeset/pre.json (mode: pre, tag: beta). Once this branch lands on
main, the release pipeline versions the pending changesets as prereleases
(unthrown 5.0.0-beta.0; the fixed group with it, orpc/prisma 0.1.1-beta.0)
and publishes under the npm "beta" dist-tag, leaving "latest" on 4.x.
Run `changeset pre exit` to graduate v5 to a stable release.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 56 out of 57 changed files in this pull request and generated 6 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file
Comments suppressed due to low confidence (1)

docs/guide/the-defect-channel.md:52

  • This example again uses P._ without defining P. Since d’s error channel is never, the vacuously-exhaustive matcher => matcher form keeps the example consistent and self-contained.
```ts
const recovered = d.recoverErr((matcher) => matcher.with(P._, () => 99));
// type: Result<number, never>

Comment thread docs/guide/boundaries.md
Comment thread docs/guide/from-try-catch.md
Comment thread docs/guide/core-concepts.md
Comment thread docs/guide/the-defect-channel.md
Comment thread docs/guide/orpc.md
Comment thread docs/api/index.md Outdated
btravers added 5 commits July 25, 2026 03:04
- Self-contain P._ snippets: vacuous `matcher => matcher` where E is never
  (core-concepts, the-defect-channel pass-throughs), literal/structural
  matches where the modeled error is concrete (from-try-catch, boundaries);
  keep P._ with a visible import only where the branch supplies the U type.
- Fix broken ./pattern-matching links (guide removed in 18783bc): link
  ts-pattern to its repo (core-concepts, orpc); drop testing's dangling
  Continue-to (it was the terminal page).
- Drop matchTags from the hand-written API overview.
…gregates

BREAKING CHANGES:
- Remove the deprecated `unwrap`/`unwrapErr`/`unwrapOr`/`unwrapOrElse`
  aliases (runtime-identical delegates). The extractor family is spelled only
  `get…`. One concept, one name — no deprecated surface survives into v5.
- Gate `getOrThrow` as the complement of `get`: it now compiles only when the
  error channel is non-empty (E ≠ never). On `Result<T, never>` there is
  nothing to throw, so `get()` is the tool. `get`/`getOrThrow` partition
  extraction by the error channel's state.

Hardening (non-breaking):
- `allAsync`/`allFromDictAsync` adopt each input defensively, so a cast/untyped
  rejecting thenable becomes a Defect rather than rejecting the internal promise
  (upholds 'the internal promise never rejects' for out-of-contract input).
  Regression tests added.
- Document the flatTapErr asymmetry (a branch returning a Defect replaces the
  Err; only a throw aggregates). Fix UnwrapError's stale 'unwrap' message.

Audit-driven: the getOrThrow gate and unwrap removal were the design audit's
recommendations; the aggregate rejection gap was the robustness audit's one
real invariant finding (verified by repro, now tested).
… coverage)

From the documentation accuracy audit:
- from-try-catch: the escape-hatch example used get() on a fallible Result
  (won't compile — get() is gated to E = never); switch it and the surrounding
  prose to getOrThrow(), the ungated extractor that throws the modeled error.
- Correct the stale 'zero runtime dependencies' claim (core depends on
  ts-pattern) in getting-started, docs/index, the comparison table, core README.
- the-defect-channel: add tapDefect/tapFailure to the defect pass-through
  exception list.
- Add toBeErrWith to the vitest matcher lists (testing guide, vitest README,
  API index); add the missing @unthrown/prisma entry and round out the core
  bullet in the API index; fix the effect README match example to the matcher
  form.
With the unwrap* aliases gone, UnwrapError was the last 'unwrap' vocabulary in
the surface. It's what the get… extractors throw on a wrong-variant access, so
it now lives in the get register. Unreachable through well-typed code (only a
cast/JS caller reaches it), so migration is a one-line rename for the few who
instanceof/catch it.

BREAKING CHANGE: the exported UnwrapError class is renamed to GetError.
The four aggregates read each element's .tag; a hole/undefined element (only
reachable via untyped/cast input) made sync all()/allFromDict() throw a raw
TypeError and made allAsync()/allFromDictAsync() REJECT — the latter violating
'an AsyncResult's internal promise never rejects'. Guard the fold: a non-Result
element becomes a Defect (an unexpected failure), uniformly across sync and
async. Completes the earlier rejecting-thenable hardening (Array.map skipped
holes, so they still reached the fold). Regression tests for all four.
@btravers
btravers merged commit 3cbdec7 into main Jul 25, 2026
14 checks passed
@btravers
btravers deleted the feat/error-channel-triage branch July 25, 2026 11:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants