Rust-style Result types for TypeScript — the value half of
always-panic.
Everything here stands alone: E can be any type (a string, a plain
Error, a discriminated union, …). Nothing requires TypedError — the
typed-error half builds on top of these
types, not the other way around.
import { ok, err, type Result } from 'always-panic'
function parsePort(raw: string): Result<number, string> {
const n = Number(raw)
if (!Number.isInteger(n) || n < 0 || n > 65535) return err(`bad port: ${raw}`)
return ok(n)
}
const port = parsePort('8080').unwrapOr(3000)
Result<T, E>A discriminated union of Ok<T> and Err<E>, modeled after
Rust's Result.
Create values with ok(value) and err(error); narrow with isOk() /
isErr().
const r = parsePort(input)
if (r.isOk()) {
r.value // number
} else {
r.error // string
}
Common methods:
| Method | Ok | Err |
|---|---|---|
isOk() / isErr() |
type guard | type guard |
expect(msg) |
returns value | throws Error with msg |
unwrap() |
returns value | throws Error |
unwrapErr() |
throws | returns error |
unwrapOr(default) |
returns value | returns default |
unwrapOrElse(fn) |
returns value | calls fn(error) |
map(fn) / mapErr(fn) |
maps value / unchanged | unchanged / maps error |
mapOr / mapOrElse |
compute from value | return default or call fn(error) |
and / andThen(fn) |
pass through / chain Result |
unchanged |
or / orElse(fn) |
unchanged | pass through / fallback Result |
inspect(fn) / inspectErr(fn) |
side effect on value / unchanged | unchanged / side effect on error |
inspect / inspectErr callbacks receive DeepReadonly<T> — a compile-time
readonly view; nothing is frozen at runtime.
On Err, unwrap() and expect() attach the inner error as Error.cause
when it opts in with causeForUnwrap: true (the error half's
UnexpectedError is the built-in error that does).
result.gen (the ? operator)result.gen runs a generator body in which yield* res either evaluates to
the Ok value or short-circuits the whole body with the Err — the
equivalent of Rust's
? operator.
No more if (r.isErr()) return r ladders:
import { ok, result } from 'always-panic'
// sync — Result out
const r = result.gen(function* () {
const user = yield* findUser(id) // Result<User, DbError>
const posts = yield* findPosts(user) // Result<Post[], DbError | CacheError>
return ok({ user, posts })
}) // Result<{ user: User; posts: Post[] }, DbError | CacheError>
// async — AsyncResult out; yield* works on AsyncResult directly
const ar = result.gen(async function* () {
const user = yield* fetchUser(id) // AsyncResult<User, HttpError>
const posts = yield* fetchPosts(user) // AsyncResult<Post[], HttpError>
return ok({ user, posts })
})
return a Result (ok(...) or err(...)). Error types from every yield* and from the returned Result accumulate in the resulting error union.Err is returned by reference — identity and stack preserved, exactly like an early return r.finally blocks and using / await using disposals in the body still run..try().Result out; async in, AsyncResult out. In an async body yield* accepts both AsyncResults and awaited Results; sync Results work in either. Explicit variants: genSync / genAsync.result namespaceimport { result } from 'always-panic'
result.ok(value) / result.err(error) — also exported top-level as
ok / err. ok() with no argument is Ok(undefined), typed
Result<void, never> — the success case of a Result<void, E>.result.all([...]) — combine sync Results into
Result<[values], E>; short-circuits on the first Err by array order.result.isResult(value) — runtime guard: value instanceof Ok | Err.result.fromMaybe(value) — normalize a MaybeResult<T, E> (a value
that may or may not already be a Result): a Result is returned by
reference, anything else is wrapped in Ok.result.gen(body) (and genSync / genAsync) — early return via
yield*; see above.result.asIs(res) — identity helper that widens a merged union like
Ok<A> | Err<B> | Err<C> back to Result<A, B | C> for type assertions.result.panic(res) (and panicSync / panicAsync) — the one bridge
to the typed-error half: unwraps (throws)
when res is Err(UnexpectedError) and removes UnexpectedError from the
error union otherwise. Irrelevant unless you use TypedError; see the
main README and CORE_CONCEPTS.AsyncResult<T, E>Thenable wrapper around Promise<Result<T, E>>. await asyncResult yields a
Result. Chain with map, mapErr, and, andThen, or, orElse,
inspect, inspectErr — the callbacks may return sync or async values.
import { AsyncResult, ok } from 'always-panic'
const ar = AsyncResult.from(async () => ok(await fetchUser('1')))
const mapped = await ar.map((user) => user.name)
const name = mapped.unwrapOr('anonymous')
Construct from a Result, a PromiseLike<Result<...>>, or a function
returning either:
AsyncResult.from(ok(1))
AsyncResult.from(Promise.resolve(ok(1)))
AsyncResult.from(async () => ok(await load()))
AsyncResult.all — fail-fast. Returns Err(e) as soon as the first
input settles to Err (by completion time, not array index). Unlike
Promise.all, an Err value resolves the outer async result instead of
rejecting it; only a rejected underlying promise rejects (and only if that
rejection wins the race before an Err settles).
AsyncResult.merge — waits for every input to settle as a Result,
then returns the first Err by array order (or Ok([...]) if all
succeeded). Underlying promise rejections still reject the merge via
Promise.all (it does not treat rejections as Err values).
const allOk = await AsyncResult.all([fetchA(), fetchB()])
const [a, b] = allOk.unwrap()
const merged = await AsyncResult.merge([fetchA(), fetchB()])
const [c, d] = merged.unwrap()
// values
export { Ok, Err, ok, err, result, AsyncResult }
// types
export type {
Result,
ResultBase,
ResultLike,
OkContent,
ErrContent,
DeepReadonly,
MaybeResult,
MaybeOkContent,
ResultOkTypes,
ResultErrTypes,
AsyncResultOkTypes,
AsyncResultErrTypes,
}