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
14 changes: 7 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,8 +402,8 @@ was planned).
- **`get()` / `getErr()` are type-gated.** `get()` compiles only when the
error channel is empty; `getErr()` only when the success channel is empty.
The gate is `getOrThrow`'s shape — `this: [E] extends [never] ? Result<T,
never> : "unthrown: get() needs an empty error channel …"` (and `[T] extends
[never]` for `getErr`) — so the compile error prints the fix rather than a
never> : "unthrown: get() needs an empty error channel …"` (and `[T] extends
[never]` for `getErr`) — so the compile error prints the fix rather than a
`DefectView` mismatch; the `Result<T, never>` semantics (and a generic `T`
with an empty `E`) are unchanged. Eliminate the opposite
channel first (`match` / `recoverErrCases` / `flatMapErrCases`), or use the `getOr` /
Expand Down Expand Up @@ -460,7 +460,7 @@ async work re-enters via `fromPromise` / `fromSafePromise` and composes with
where `M extends ExhaustiveMatch<…>` (the callback returns the un-terminated
builder; the combinator runs `.run()`). Outgoing types are computed from the
builder output `MatchOut<M>` — mapErrCases: `MatchErrOut<M>` (= `Exclude<MatchOut,
Defect>`); flatMapErrCases: `OkOf`/`ErrOf` — plus `AsyncOkOf`/`AsyncErrOf` on the
Defect>`); flatMapErrCases: `OkOf`/`ErrOf` — plus `AsyncOkOf`/`AsyncErrOf` on the
async surface — over it; recoverErrCases: `T | MatchErrOut<M>` with `E = never`.
`flatTapErrCases` is the exception: it infers a plain `E2` from the builder output
and returns `E | E2` (the `MatchOut<M>` shape unioned with the class `E`
Expand Down Expand Up @@ -538,7 +538,7 @@ Defect>`); flatMapErrCases: `OkOf`/`ErrOf` — plus `AsyncOkOf`/`AsyncErrOf` on
the same reason `ErrMatcher` and `@unthrown/prisma`'s `TransactionClient<C>`
are: a helper factored out of the callback has to be able to name its
parameter, and deriving it from the function (`Parameters<typeof
fromExecutor<T, E>>[0]`) is the grotesque spelling that invites a hand-copied
fromExecutor<T, E>>[0]`) is the grotesque spelling that invites a hand-copied
drifting duplicate instead.
`ErrMatcher<E>` — the built-in match builder over the error
(`ReturnType<typeof match<E>>`, i.e. `Matcher<E, E, never>`).
Expand Down Expand Up @@ -692,7 +692,7 @@ fromExecutor<T, E>>[0]`) is the grotesque spelling that invites a hand-copied
`TaggedErrorConstructor` / `TaggedErrorInstance` are exported too — they type
an `extends TaggedError(…)` site from the outside. **There is no `matchTags`** — a per-tag
exhaustive fold over a tagged error union is `result.match({ ok, defect, errCases:
(matcher) => matcher.with(P.tag("…"), …) })`, which generalises beyond `_tag` to
(matcher) => matcher.with(P.tag("…"), …) })`, which generalises beyond `_tag` to
any discriminant.

Deliberately **excluded** for now: **generator** do-notation (`gen`/`yield*`
Expand Down Expand Up @@ -770,7 +770,7 @@ library can be "done".
`AsyncResultMethods` re-imposes the precision, the same pattern as `get`'s
re-imposed `this` gate. `AsyncOkOf`/`AsyncErrOf` infer through the `Awaitable` `then`
channel only (`R extends Awaitable<infer Res>`), NOT `R extends
AsyncResult<infer T, …>` — structural inference over the whole method surface
AsyncResult<infer T, …>` — structural inference over the whole method surface
would pick up junk candidates. The **plain async binds** (`flatMap` / `flatTap`
/ `bind`) hit the same hazard from the other direction: destructuring `U`/`E2`
through an `AsyncResult<U, E2>` union member in the callback-return position
Expand Down Expand Up @@ -1031,7 +1031,7 @@ channel?**
`all`). `toEither` has no defect target, so it takes a mandatory `onDefect`.
- **neverthrow and Boxed do not.** Coming _in_, results are only `Ok`/`Err` —
never a `Defect`. Going _out_, every `to*` takes a **mandatory `onDefect:
(cause) => E`** (forced triage, Thesis #3): a defect is never silently folded
(cause) => E`** (forced triage, Thesis #3): a defect is never silently folded
into `E`. There is no one-arg form.
- The async-direction names are **deliberately asymmetric** —
`toNeverthrowAsync` / `toBoxedFuture` — because each names the neighbour's
Expand Down
4 changes: 2 additions & 2 deletions docs/how-to/handle-results-at-the-edge.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,10 @@ path param, `flatMap` feeds the parsed id into a repository call that returns it
own `AsyncResult`, and `match`'s matcher folds every tag to a status code:

```ts
import { Hono } from "hono";
import { z } from "zod";
import { fromSchema } from "@unthrown/standard-schema";
import { Hono } from "hono";
import { P, TaggedError, type AsyncResult } from "unthrown";
import { z } from "zod";

class InvalidId extends TaggedError("InvalidId") {}

Expand Down
4 changes: 2 additions & 2 deletions docs/how-to/interoperate-with-libraries.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ libraries have only two. That single difference decides every signature.
triage it with a mandatory `onDefect: (cause) => E`. There is no one-arg form.

```ts
import { Ok } from "unthrown";
import { toNeverthrow } from "@unthrown/neverthrow";
import { Ok } from "unthrown";

// onDefect is required — the compiler will not let you drop a defect.
toNeverthrow(Ok(1), (cause) => ({ _tag: "Bug", cause }));
Expand All @@ -39,9 +39,9 @@ Effect is the exception: it _does_ have a defect channel (`Cause.die`), so
`Result ↔ Exit` round-trips losslessly.

```ts
import { Ok, Err, P, TaggedError } from "unthrown";
import { toExit, fromEffect } from "@unthrown/effect";
import { Effect } from "effect";
import { Ok, Err, P, TaggedError } from "unthrown";

class NotFound extends TaggedError("NotFound") {}
class Timeout extends TaggedError("Timeout") {}
Expand Down
1 change: 1 addition & 0 deletions docs/how-to/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public surface. The recipes below share this setup:

```ts
import { setTimeout as sleep } from "node:timers/promises";

import { fromSafePromise, TaggedError, type AsyncResult } from "unthrown";

class RateLimited extends TaggedError("RateLimited") {}
Expand Down
2 changes: 1 addition & 1 deletion docs/how-to/use-with-drizzle.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,8 +375,8 @@ For a boundary of your own, `qualifyPgError` **is** a `qualify` in the
`fromPromise`:

```ts
import { fromPromise } from "unthrown";
import { qualifyPgError } from "@unthrown/drizzle";
import { fromPromise } from "unthrown";

const rows = fromPromise(
() => pool.query("insert into users values ($1)", [id]),
Expand Down
6 changes: 3 additions & 3 deletions docs/how-to/use-with-orpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ defect even when it throws a declared `ORPCError`; only a returned `Err`
answers a modeled failure:

```ts
import { P } from "unthrown";
import { handlerResult } from "@unthrown/orpc/server";
import { os } from "@orpc/server";
import { handlerResult } from "@unthrown/orpc/server";
import { P } from "unthrown";
import * as z from "zod";

// repo.findPlanet: (id: string) => AsyncResult<Planet, NotFound | Conflict | Unavailable>
Expand Down Expand Up @@ -191,8 +191,8 @@ trusted; it is what validates `data` before it reaches `E`.
`call(procedure, input)`:

```ts
import { fromCall } from "@unthrown/orpc/client";
import { call } from "@orpc/server";
import { fromCall } from "@unthrown/orpc/client";

const planet = await fromCall(client.planet.find({ id })); // a client call
const seeded = await fromCall(call(find, { id: "1" })); // a server-side call
Expand Down
3 changes: 2 additions & 1 deletion docs/how-to/use-with-prisma.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ operations **alongside** the raw promise ones:

```ts
import { unthrownPrisma } from "@unthrown/prisma";

import { PrismaClient } from "./generated/prisma/client.ts";

const db = new PrismaClient({ adapter }).$extends(unthrownPrisma);
Expand Down Expand Up @@ -366,8 +367,8 @@ runs, and raw SQL. Qualify raw SQL yourself at the boundary, reusing the exporte
`qualifyPrismaError`:

```ts
import { fromPromise } from "unthrown";
import { qualifyPrismaError } from "@unthrown/prisma";
import { fromPromise } from "unthrown";

const rows = fromPromise(db.$queryRaw`SELECT 1`, qualifyPrismaError);
// ^? AsyncResult<unknown, UniqueConstraintViolation | ForeignKeyViolation | RecordNotFound>
Expand Down
2 changes: 1 addition & 1 deletion docs/how-to/validate-with-standard-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ reduces it to per-field messages. (Everywhere `E` is a real union, name the case
instead — see [Exhaustive error matching](../explanation/exhaustive-error-matching).)

```ts
import { fromSchema, type SchemaIssues } from "@unthrown/standard-schema";
import { P } from "unthrown";
import { z } from "zod";
import { fromSchema, type SchemaIssues } from "@unthrown/standard-schema";

const signupSchema = z.object({
email: z.email(),
Expand Down
4 changes: 2 additions & 2 deletions packages/boxed/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ Boxed's `Result` has two channels (`Ok`/`Error`) and no defect channel. Coming
with `onDefect` — no defect is ever silently folded into your domain error type.

```ts
import { Ok } from "unthrown";
import { toBoxed, fromBoxed } from "@unthrown/boxed";
import { Result } from "@bloodyowl/boxed";
import { toBoxed, fromBoxed } from "@unthrown/boxed";
import { Ok } from "unthrown";

toBoxed(Ok(1), (cause) => ({ _tag: "Bug", cause })); // Result.Ok(1)
fromBoxed(Result.Ok(1)); // Result<number, never>
Expand Down
12 changes: 6 additions & 6 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,7 @@ No changes in this release.
(unpinned, that combinator's constraint rejects the marker). Such a branch now
behaves like the `throw` it is the expression-position form of: the resulting
`Defect` carries an `AggregateError` of `[the branch's cause, the observed
error]`, so observing a failure still never destroys it.
error]`, so observing a failure still never destroys it.

Closes #152.

Expand All @@ -721,7 +721,7 @@ error]`, so observing a failure still never destroys it.

`tapErrCases` now takes the throw route too: a `defect(…)` branch produces a
`Defect` whose cause is an `AggregateError` of `[the branch's cause, the
observed error]`, matching `throw`'s behaviour in the same position and the
observed error]`, matching `throw`'s behaviour in the same position and the
load-bearing "a failure-observer throw preserves the original failure"
invariant. Ordinary branch return values are still discarded — the `defect`
marker never was one.
Expand Down Expand Up @@ -785,7 +785,7 @@ observed error]`, matching `throw`'s behaviour in the same position and the
channel is already empty (`E = never`) `getOrThrow()` is unnecessary — there is
nothing to throw, so `get()` is the tool. The gate previously surfaced as an
opaque `The 'this' context of type '…' is not assignable to method's 'this' of
type 'never'`. The `never` receiver now carries a message, so the diagnostic
type 'never'`. The `never` receiver now carries a message, so the diagnostic
reads:

> unthrown: getOrThrow is unnecessary here — the Err channel is empty (E =
Expand Down Expand Up @@ -899,7 +899,7 @@ type 'never'`. The `never` receiver now carries a message, so the diagnostic
(unpinned, that combinator's constraint rejects the marker). Such a branch now
behaves like the `throw` it is the expression-position form of: the resulting
`Defect` carries an `AggregateError` of `[the branch's cause, the observed
error]`, so observing a failure still never destroys it.
error]`, so observing a failure still never destroys it.

Closes #152.

Expand All @@ -911,7 +911,7 @@ error]`, so observing a failure still never destroys it.

`tapErrCases` now takes the throw route too: a `defect(…)` branch produces a
`Defect` whose cause is an `AggregateError` of `[the branch's cause, the
observed error]`, matching `throw`'s behaviour in the same position and the
observed error]`, matching `throw`'s behaviour in the same position and the
load-bearing "a failure-observer throw preserves the original failure"
invariant. Ordinary branch return values are still discarded — the `defect`
marker never was one.
Expand Down Expand Up @@ -987,7 +987,7 @@ observed error]`, matching `throw`'s behaviour in the same position and the
channel is already empty (`E = never`) `getOrThrow()` is unnecessary — there is
nothing to throw, so `get()` is the tool. The gate previously surfaced as an
opaque `The 'this' context of type '…' is not assignable to method's 'this' of
type 'never'`. The `never` receiver now carries a message, so the diagnostic
type 'never'`. The `never` receiver now carries a message, so the diagnostic
reads:

> unthrown: getOrThrow is unnecessary here — the Err channel is empty (E =
Expand Down
1 change: 1 addition & 0 deletions packages/drizzle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ for.
```ts
import { drizzle } from "@unthrown/drizzle/node-postgres";
import { eq } from "drizzle-orm";

import { usersTable } from "./schema.ts";

const db = drizzle({ client: pool, relations });
Expand Down
4 changes: 2 additions & 2 deletions packages/effect/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ failure (`Cause.fail` ↔ `Err`) from an unexpected one (`Cause.die` ↔ `Defect
So `Result ↔ Exit` is a genuine **bijection**.

```ts
import { Ok, Err, P, TaggedError } from "unthrown";
import { toExit, fromEffect, toEither } from "@unthrown/effect";
import { Effect } from "effect";
import { Ok, Err, P, TaggedError } from "unthrown";

class NotFound extends TaggedError("NotFound") {}

Expand All @@ -42,7 +42,7 @@ await fromEffect(load("1")).match({
a `Defect` **dominates** a modeled failure in a composite cause (same rule as
`all`).
- `toEither` / `fromEither` — `Either` has no defect channel, so `toEither(r,
onDefect)` **forces** you to triage the defect into `E` (Thesis #3). `fromEither`
onDefect)` **forces** you to triage the defect into `E` (Thesis #3). `fromEither`
never yields a `Defect`.
- `toEffect` / `fromEffect` — `toEffect` lifts a `Result` **or** `AsyncResult`
into an `Effect<T, E>` (`Defect → Effect.die`); `fromEffect` runs an
Expand Down
2 changes: 1 addition & 1 deletion packages/neverthrow/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ every neverthrow result is an `Ok` or `Err` — never a `Defect`. Going **out**,
`onDefect` — no defect is ever silently folded into your domain error type.

```ts
import { Ok } from "unthrown";
import { toNeverthrow, fromNeverthrow } from "@unthrown/neverthrow";
import { ok as ntOk } from "neverthrow";
import { Ok } from "unthrown";

toNeverthrow(Ok(1), (cause) => ({ _tag: "Bug", cause })); // neverthrow Ok(1)
fromNeverthrow(ntOk(1)); // Result<number, never>
Expand Down
2 changes: 1 addition & 1 deletion packages/orpc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ A callback throw or rejection is always a defect, including a declared
`ORPCError`. Return `Err(errors.CODE(...))` for a modeled failure.

```ts
import { P } from "unthrown";
import { handlerResult } from "@unthrown/orpc/server";
import { P } from "unthrown";

const find = os
.input(z.object({ id: z.string() }))
Expand Down
3 changes: 1 addition & 2 deletions packages/oxlint/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,6 @@ No changes in this release.
```ts
// import type { Result } from "unthrown";
import type { Result, AsyncResult } from "unthrown";

// import { type Result } from "unthrown";
import { type Result, type AsyncResult } from "unthrown";
```
Expand Down Expand Up @@ -389,7 +388,7 @@ No changes in this release.
- 4bf8ffc: **New rule `unthrown/no-catch-all-pattern`.** Bans the matcher catch-all `P._`
(and its alias `P.any`) wherever `P` is imported from `unthrown` or `ts-pattern`,
so every error case must be enumerated by name (`.with(P.tag("A"), P.tag("B"),
…, handler)`, grouping cases that share a handler).
…, handler)`, grouping cases that share a handler).

Resolves `P` by its imported name via scope analysis (a rename still fires; a
decoy does not); a namespace import (`ns.P._`) is a documented limit. It is part
Expand Down
2 changes: 1 addition & 1 deletion packages/oxlint/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@oxlint/plugins": "^1.80.0"
"@oxlint/plugins": "^1.87.0"
},
"devDependencies": {
"@btravstack/tsconfig": "catalog:",
Expand Down
3 changes: 2 additions & 1 deletion packages/prisma/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ type_. Qualification happens once, inside the extension: no raw Promise ever
reaches your code.

```ts
import { P } from "unthrown";
import { unthrownPrisma } from "@unthrown/prisma";
import { P } from "unthrown";

import { PrismaClient } from "./generated/prisma/client.ts";

const db = new PrismaClient({ adapter }).$extends(unthrownPrisma);
Expand Down
Loading
Loading