# better-result > Typed, composable error handling for TypeScript with Result values, tagged errors, and generator composition. # A better Result type for TypeScript Source: https://better-result.dev/ TypeScript-first zero runtime dependencies `better-result` makes expected failure explicit without turning your application into nested conditionals. A value is either `Ok` or `Err`, and TypeScript carries both possibilities to the place where you decide what to do. ```ts import { Result, TaggedError } from "better-result"; class InvalidPort extends TaggedError("InvalidPort")<{ input: string; message: string; }> {} const parsePort = (input: string) => { const port = Number(input); return Number.isInteger(port) && port > 0 ? Result.ok(port) : Result.err(new InvalidPort({ input, message: "Port must be a positive integer" })); }; const address = parsePort("3000").map((port) => `http://localhost:${port}`); // Result ``` Success and error types stay visible through every transformation. Use `yield*` to write multi-step workflows without callback pyramids. Expected failures are `Err`; thrown callback defects become `Panic`. ## Start in sixty seconds 1. **Install** ```sh npm install better-result ``` 2. **Create a Result** ```ts const parsed = Result.try(() => JSON.parse(input)); // Result ``` 3. **Handle both branches** ```ts const message = parsed.match({ ok: (value) => `Parsed ${JSON.stringify(value)}`, err: (error) => `Could not parse: ${error.message}`, }); ``` ## Choose your path Learn the model, boundaries, and difference between recoverable errors and defects. Build a small typed workflow and see the inferred error union. Browse every `Result` constructor and combinator. Migrate tagged errors, serialization, recovery, matching, and retries. ## One contract for people and tools Every page is also emitted as clean Markdown. The site publishes [`/llms.txt`](/llms.txt), [`/llms-full.txt`](/llms-full.txt), page-level `.md` mirrors, searchable headings, and literal API names. Humans get navigation and examples; coding agents get the same technical contract without scraping presentation markup. > **The shortest useful rule** > > Return `Err` for a failure the caller can reasonably handle. Let `Panic` expose bugs and broken > invariants. --- # Async operations and retries Source: https://better-result.dev/core/async-and-retries For application workflows, prefer [`Result.gen` with `Result.await`](/core/generator-composition#asynchronous-workflows). For a short `Promise>` pipeline, chain [static async combinators](/core/transforming-and-chaining) with `.then(Result.andThenAsync(...))`. This page focuses on safely creating those asynchronous Results and controlling retries. ## Capture a Promise ```ts const result = await Result.tryPromise({ try: () => fetch(url), catch: (cause) => new RequestFailed({ url, cause, message: "Request failed" }), }); ``` The attempt context always includes `attempt`, starting at 1. Its optional `signal` is the exact signal supplied in the top-level configuration; when the caller supplies none, `signal` is `undefined`. `Result.tryPromise` does not create an internal abort signal. ## Handle fulfilled HTTP errors `fetch` rejects for network and cancellation failures, not for HTTP error statuses. Check fulfilled responses explicitly: ```ts class HttpResponseError extends TaggedError("HttpResponseError")<{ status: number; url: string; message: string; }> {} const successfulResponse = await Result.tryPromise({ try: () => fetch(url), catch: (cause) => new NetworkError({ url, cause, message: "Network request failed" }), }).then( Result.andThen((response: Response) => response.ok ? Result.ok(response) : Result.err( new HttpResponseError({ status: response.status, url: response.url, message: `Request failed with status ${response.status}`, }), ), ), ); // Result ``` A retry configured on `Result.tryPromise` retries rejected attempts. If selected HTTP statuses are also retryable, convert and retry them in an application operation whose policy explicitly covers status responses. ## Static retry policy ```ts const result = await Result.tryPromise(callApi, { retry: { times: 3, delayMs: 100, backoff: "exponential", }, }); ``` `times` counts retries after the initial attempt. Static policies require `delayMs` and one of: | Backoff | Delay before retry number `n` | | ------------- | ----------------------------- | | `constant` | `delayMs` | | `linear` | `delayMs × n` | | `exponential` | `delayMs × 2^(n - 1)` | ## Retry selected errors ```ts retry: { times: 3, delayMs: 200, backoff: "exponential", shouldRetry: (error, { attempt }) => error._tag === "RateLimited" && attempt < 4, } ``` `shouldRetry` is synchronous. If it throws, `Result.tryPromise` throws `Panic`. ## Error-dependent delays ```ts retry: { times: 3, shouldRetry: (error) => error.retryable, delayMs: (error, { attempt }) => error.retryAfterMs ?? attempt * 250, } ``` A dynamic `delayMs` is the final delay and cannot be combined with `backoff` or `jitter`. A throwing callback becomes `Panic`. ## Jitter ```ts retry: { times: 3, delayMs: 100, backoff: "exponential", jitter: 0.3, } ``` A numeric jitter from `0` through `1` shortens a static delay by up to that fraction. `true` means full jitter. Invalid, non-finite, or out-of-range numbers throw `Panic` before the first attempt. ## Cancellation ```ts const controller = new AbortController(); const result = await Result.tryPromise(({ signal }) => fetch(url, { signal }), { signal: controller.signal, retry: { times: 3, delayMs: 100, backoff: "constant" }, }); ``` Aborting stops a pending retry delay and prevents later attempts. The active operation is cancelled only if your callback forwards `signal` to an abort-aware API. The latest typed `Err` is returned when retry scheduling stops. > **Retries need an idempotency decision** > > Do not retry a state-changing operation merely because the API allows it. Decide whether the > operation and its transport are safe to repeat. --- # Collections Source: https://better-result.dev/core/collections ## `Result.all` Collect all successes or return the first error in input order: ```ts const result = Result.all([Result.ok(1), Result.ok("two"), Result.ok(true)] as const); // Result<[number, string, boolean], never> ``` ```ts const result = Result.all([loadUser(), loadTeam(), loadPlan()]); // first Err short-circuits collection ``` The returned error is the first error encountered while iterating. ## `Result.allAsync` Await all inputs concurrently, then collect in input order: ```ts const result = await Result.allAsync([ fetchUser(id), fetchTeam(teamId), Result.ok(cachedPolicy), ] as const); ``` A raw rejected input Promise is a broken contract and becomes `Panic`. Promise-returning operations should normally catch expected rejection with `Result.tryPromise` before collection. ## `Result.partition` Keep every branch and preserve relative order: ```ts const [users, errors] = Result.partition(results); // users: Array // errors: Array ``` Unlike `all`, partition never short-circuits. ## `Result.partitionAsync` ```ts const [users, errors] = await Result.partitionAsync(requests); ``` Inputs are awaited concurrently, then partitioned in input order. A rejected Promise becomes `Panic`. ## `Result.flatten` ```ts const nested: ResultType, LoadError> = loadNested(); const flat = Result.flatten(nested); // Result ``` Prefer `andThen` when creating the chain. `flatten` is useful when a nested Result already exists. --- # Creating Results Source: https://better-result.dev/core/creating-results ## `Result.ok` ```ts const count = Result.ok(3); // Ok const done = Result.ok(); // Ok ``` An `Ok` stores its payload in `.value` and has `status: "ok"`. ## `Result.err` ```ts const missing = Result.err(new UserNotFound({ id })); // Err ``` An `Err` stores its payload in `.error` and has `status: "error"`. Prefer meaningful error objects over strings at module boundaries. ## `Result.try` Capture a synchronous throwing API: ```ts const parsed = Result.try(() => JSON.parse(input)); // Result ``` Without a custom `catch`, the original thrown value is available as `UnhandledException.cause`. Map the exception into your domain: ```ts class InvalidJson extends TaggedError("InvalidJson")<{ cause: unknown; message: string; }> {} const parsed = Result.try({ try: () => JSON.parse(input), catch: (cause) => new InvalidJson({ cause, message: "Input is not valid JSON" }), }); // Result ``` The callback receives `{ attempt }`. Synchronous retry runs immediately and is bounded by `times`: ```ts const value = Result.try(({ attempt }) => readFlakyValue(attempt), { retry: { times: 2 }, // one initial attempt, at most two retries }); ``` ## `Result.tryPromise` Capture a Promise-returning operation: ```ts const response = await Result.tryPromise(() => fetch(url)); // Result ``` Use an object to create a typed error: ```ts const response = await Result.tryPromise({ try: () => fetch(url), catch: (cause) => new NetworkError({ cause, url, message: "Request failed" }), }); ``` Retries, delays, backoff, jitter, and cancellation are covered in [Async operations and retries](/core/async-and-retries). > **Catch handlers must not fail** > > If a custom `catch` callback throws or rejects, better-result throws a `Panic`. A catch handler is > the boundary that promised to convert an unknown exception into a known error. --- # Extracting values Source: https://better-result.dev/core/extracting-values ## Prefer `match` at handling boundaries ```ts const response = result.match({ ok: (value) => Response.json(value, { status: 200 }), err: (error) => toErrorResponse(error), }); ``` Both branches are visible and the output type is unified. ## Provide a fallback with `unwrapOr` ```ts const port = parsePort(input).unwrapOr(3000); // number ``` The fallback may have a different type, so the result is the union of the success and fallback types. Static forms: ```ts Result.unwrapOr(result, defaultValue); Result.unwrapOr(defaultValue)(result); ``` ## Assert success with `unwrap` ```ts const value = result.unwrap(); const value = result.unwrap("Configuration must be valid at startup"); ``` On `Err`, `unwrap` throws `Panic`; the error value is preserved as its cause. Use this only where an `Err` proves an invariant is broken or where an outer defect boundary intentionally terminates the operation. ## Do not unwrap routine failures Avoid: ```ts const user = findUser(id).unwrap(); // missing user is expected ``` Handle or propagate instead: ```ts const greeting = findUser(id).map((user) => `Hello, ${user.name}`); ``` > **unwrap is an assertion** > > `unwrap` is not a substitute for error handling. Its meaning is: “an Err here is a defect.” --- # Generator composition Source: https://better-result.dev/core/generator-composition `Result.gen` turns generator syntax into railway-style composition. `yield*` unwraps `Ok` values and stops at the first `Err`. Prefer `Result.gen` for application workflows with several fallible steps. For asynchronous workflows, pair it with `Result.await` so intermediate values stay local and every yielded error remains in the inferred union. ## Synchronous workflows ```ts const checkout = Result.gen(function* () { const cart = yield* loadCart(cartId); const stock = yield* reserveStock(cart.items); const order = yield* createOrder(cart, stock); return Result.ok(order); }); // Result ``` Always return `Result.ok(...)` or `Result.err(...)` from the generator. Returning a bare value is a defect and throws `Panic`. ## Yield tagged errors directly Every `TaggedError` is iterable, so guard clauses stay small: ```ts const authorize = (user: User) => Result.gen(function* () { if (!user.active) { yield* new InactiveUser({ userId: user.id, message: "User is inactive" }); } return Result.ok(user); }); ``` This is equivalent to `yield* Result.err(new InactiveUser(...))`. ## Asynchronous workflows This is the preferred style for composing several `Promise>` operations. Use an async generator and wrap each Promise with `Result.await`: ```ts const dashboard = await Result.gen(async function* () { const session = yield* Result.await(readSession()); const user = yield* Result.await(fetchUser(session.userId)); const posts = yield* Result.await(fetchPosts(user.id)); return Result.ok({ user, posts }); }); ``` Do not write `yield* await fetchUser()`. `Result.await` supplies the async iterable protocol needed by the generator and preserves the error type. ## Cleanup behavior When an `Err` short-circuits, `Result.gen` closes the generator so `finally`, `Symbol.dispose`, and `Symbol.asyncDispose` cleanup can run. If generator body or cleanup code throws, `Result.gen` throws `Panic`. ```ts const result = Result.gen(function* () { using resource = openResource(); return Result.ok(yield* readResource(resource)); }); ``` ## Normalize an error union Translate the completed workflow when callers should see one abstraction-level error: ```ts const result = checkout.mapError( (cause) => new CheckoutFailed({ stage: cause._tag, cause, message: "Checkout failed" }), ); ``` > **Keep generators declarative** > > Put effects and validation in named Result-returning functions. The generator should read as a > workflow, not contain every implementation detail. --- # Narrowing and matching Source: https://better-result.dev/core/narrowing-and-matching ## Discriminant narrowing ```ts if (result.status === "ok") { result.value; } else { result.error; } ``` `status` is the serializable discriminant: `"ok"` or `"error"`. ## Static guards ```ts if (Result.isOk(result)) { use(result.value); } if (Result.isError(result)) { report(result.error); } ``` ## Instance guards ```ts if (result.isOk()) { use(result.value); } if (result.isErr()) { report(result.error); } ``` `isErr()` is the instance spelling; `Result.isError()` is the static spelling. ## `match` Use `match` when both branches produce the same output type and you are ready to leave `Result`: ```ts const statusCode = result.match({ ok: () => 200, err: (error) => (error._tag === "NotFound" ? 404 : 500), }); ``` Static data-first and data-last forms are also available: ```ts Result.match(result, { ok: (value) => value.name, err: () => "Anonymous", }); const displayName = Result.match({ ok: (value: User) => value.name, err: (_error: FindUserError) => "Anonymous", }); displayName(result); ``` ## Match Result first, then tagged errors ```ts const response = result.match({ ok: (user) => ({ status: 200, body: user }), err: (error) => error.match({ UserNotFound: () => ({ status: 404, body: null }), DatabaseUnavailable: () => ({ status: 503, body: null }), }), }); ``` This keeps the two decisions explicit: Result branch first, error variant second. > **Thrown handlers are defects** > > A throwing `match` handler becomes `Panic`; return a value or a Result from a different combinator > instead of throwing expected failures. --- # Observing Results Source: https://better-result.dev/core/observing Observation methods return the original Result unchanged. ## Observe success ```ts const result = parsePayload(input).tap((payload) => { metrics.increment("payload.parsed"); logger.debug("Parsed payload", { id: payload.id }); }); ``` Use `tapAsync` for an async side effect. ## Observe errors ```ts const result = parsePayload(input).tapError((error) => { metrics.increment("payload.parse_failed", { tag: error._tag }); }); ``` Use `tapErrorAsync` for an async side effect. ## Observe either branch ```ts const result = loadUser(id).tapBoth({ ok: (user) => logger.info("Loaded user", { userId: user.id }), err: (error) => logger.warn("Could not load user", { tag: error._tag }), }); ``` For a `Promise>`, chain the static async combinator instead of awaiting only to call an instance method: ```ts const observed = await fetchUser(userId).then( Result.tapBothAsync({ ok: (user: User) => trace("user.loaded", { userId: user.id }), err: (error: FetchUserError) => trace("user.load_failed", { tag: error._tag }), }), ); ``` `tapBothAsync` selects one Promise-returning handler and preserves the original Result. ## Static and data-last forms ```ts Result.tapError(result, reportError); const withErrorReporting = Result.tapError((error: AppError) => reportError(error)); const observed = withErrorReporting(result); ``` ## Safety contract A throwing or rejected observer becomes `Panic`. Observation should not be able to quietly change the Result's error type. If telemetry failure is expected and recoverable, model that telemetry operation separately rather than hiding it in `tap`. > **Keep payloads safe** > > A typed error may still contain secrets or personal data. Select fields deliberately before > sending errors or Result values to logs and tracing systems. --- # Transforming and chaining Source: https://better-result.dev/core/transforming-and-chaining ## Transform success with `map` ```ts const name = findUser(id).map((user) => user.name); // Result ``` `map` runs only for `Ok`. Returning a Result from `map` nests it; use `andThen` to flatten a Result-returning callback. ## Transform errors with `mapError` ```ts const user = queryUser(id).mapError( (error) => new LoadUserFailed({ cause: error, message: "Could not load user" }), ); // Result ``` Use this at abstraction boundaries to translate lower-level errors into the vocabulary the caller owns. ## Chain with `andThen` ```ts const result = parseInput(raw).andThen(validateInput).andThen(saveInput); // Result ``` `andThen` runs only for `Ok` and unions the existing and next error types. Use the static `Result.andThenAsync` when the next operation returns a Promise of Result: ```ts const result = await Result.andThenAsync(parseInput(raw), (input) => saveInput(input)); ``` ## Compose `Promise` pipelines When an operation already returns `Promise>`, Promise chaining composes with static, data-last combinators: ```ts const postCount = await fetchUser(userId) .then(Result.andThenAsync((user: User) => fetchPosts(user.id))) .then(Result.map((posts: ReadonlyArray) => posts.length)); // Result ``` `Promise.then` unwraps each outer Promise. `Result.andThenAsync` runs the next asynchronous operation only for `Ok`, while `Result.map` transforms the eventual success. Both error types remain visible. Use this form for a short pipeline. Prefer [`Result.gen` with `Result.await`](/core/generator-composition#asynchronous-workflows) when a workflow has several named steps or needs local intermediate values. ## Recover with `tryRecover` Recovery runs only for `Err` and must return a Result: ```ts const user = findUser(id).tryRecover((error) => error._tag === "UserNotFound" ? Result.ok(guestUser) : Result.err(error), ); ``` Recovery may introduce a different success type. The existing success is preserved: ```ts const recovered = loadRemoteConfig().tryRecover(() => Result.ok(defaultConfigPath)); // Result ``` Use `tryRecoverAsync` when recovery returns `Promise>`: ```ts const user = await fetchUser(userId).then( Result.tryRecoverAsync(async (error: FetchUserError) => error._tag === "NetworkUnavailable" ? await readCachedUser(userId) : Result.err(error), ), ); // Result ``` The async callback still returns a Result. Rejection is a callback defect and becomes `Panic`; capture expected Promise failure inside the recovery operation. ## Static and pipeable forms Most combinators support data-first and data-last calls: ```ts Result.map(result, (value) => value.id); Result.map((value: User) => value.id)(result); Result.andThen(result, validateUser); Result.andThen(validateUser)(result); ``` This supports method chains, direct calls, and functional pipelines without separate APIs. ## Callback safety Callbacks passed to mapping, chaining, and recovery combinators are assumed not to throw. If one does, the operation throws a `Panic` with the original exception as `cause`. --- # Matching errors Source: https://better-result.dev/errors/matching-errors ## Exhaustive instance matching Every `TaggedError` instance has a `.match()` method: ```ts const message = error.match({ UserNotFound: (error) => `No user ${error.userId}`, PermissionDenied: (error) => `Missing permission ${error.permission}`, }); ``` Every `_tag` in the receiver union needs a handler. Handler parameters narrow to their exact error class. Different handler outputs produce a return union. ```ts const outcome = error.match({ UserNotFound: () => 404, PermissionDenied: () => "forbidden" as const, }); // number | "forbidden" ``` This composes directly inside a `Result` error branch without annotating the inferred error: ```ts const response = result.match({ ok: (user) => ({ status: 200, body: user }), err: (error) => error.match({ UserNotFound: () => ({ status: 404, body: null }), DatabaseUnavailable: () => ({ status: 503, body: null }), }), }); ``` ## Standalone `matchError` Use `matchError(error, handlers)` when working with structurally tagged errors that were not created by `TaggedError`: ```ts const message = matchError(error, { UserNotFound: (error) => `No user ${error.userId}`, PermissionDenied: (error) => `Missing permission ${error.permission}`, }); ``` The standalone function has the same exhaustive narrowing, return inference, and defect behavior as `.match()`. If the selected exhaustive handler throws, `.match()` and `matchError` throw `Panic` with the original exception as `cause`. `match` is a reserved `TaggedError` instance name. The factory rejects payload property types named `match`, preventing constructor assignment from shadowing the method. ## Data-last matching Annotate variant-specific parameters when the error union is not known until application: ```ts const toMessage = matchError({ UserNotFound: (error: UserNotFound) => `No user ${error.userId}`, PermissionDenied: (error: PermissionDenied) => error.message, }); const message = toMessage(error); ``` ## Partial matching with identity fallback `matchErrorPartial` returns unhandled errors unchanged by default: ```ts const transformed = matchErrorPartial(error, { UserNotFound: (error) => `No user ${error.userId}`, }); // string | PermissionDenied ``` This makes incremental handling explicit instead of discarding the rest of the union. ## Custom fallback ```ts const message = matchErrorPartial( error, { UserNotFound: (error) => `No user ${error.userId}` }, (unhandled) => `Unexpected failure: ${unhandled.message}`, ); ``` ## Recover selected variants Pass `Result.err` as the fallback to preserve unhandled variants while recovering one: ```ts const recovered = result.tryRecover( matchErrorPartial( { UserNotFound: (_error: UserNotFound) => Result.ok(guestUser), }, Result.err, ), ); ``` ## Match at boundaries Keep domain functions returning typed errors. Match where the application can choose an HTTP status, CLI exit code, user message, retry policy, or compensating action. --- # Panic and defects Source: https://better-result.dev/errors/panic-and-defects `Panic` represents an unrecoverable defect: user-supplied code broke a combinator contract or an asserted invariant failed. It is thrown, not returned as `Err`. ## Why Panic exists If `.map()` silently converted every thrown callback into `Err`, `Result` would stop describing expected failures accurately. Panic keeps domain errors typed and defects loud. ## Common Panic sources - callbacks passed to `map`, `mapError`, `andThen`, recovery, Result or TaggedError `match`, `matchError`, or observation throw; - async callbacks reject; - `Result.gen` body or cleanup throws; - a generator returns a non-Result value; - `unwrap()` sees `Err`; - a retry predicate or dynamic delay throws; - retry jitter is invalid; - an input Promise to `allAsync` or `partitionAsync` rejects; - a codec schema throws or rejects instead of returning validation issues; - `serializeUnsafe` receives `ResultSerializationError`, or `deserializeUnsafe` receives `ResultDeserializationError`. ## Detect and report ```ts import { isPanic, Panic } from "better-result"; try { runApplication(); } catch (error) { if (isPanic(error)) { reportDefect(error.message, error.cause); throw error; } } ``` Equivalent guards: ```ts Panic.is(value); isPanic(value); value instanceof Panic; ``` `Panic` has `_tag: "Panic"`, `message`, `cause`, stack chaining, and `toJSON()`. ## Throw one deliberately ```ts import { panic } from "better-result"; const unreachable = (value: never): never => panic("Unexpected application state", value); ``` Use this for impossible states and failed invariants, not ordinary validation. ## Boundary guidance Catch Panic only at a true defect boundary—process entry point, request crash reporter, worker supervisor, or test assertion. Log/report it with appropriate redaction, then usually let the current operation fail. > **Do not turn Panic back into a generic Err** > > Broadly catching Panic and returning `Err("something went wrong")` hides defects and makes typed > error contracts misleading. --- # Tagged errors Source: https://better-result.dev/errors/tagged-errors `TaggedError` creates real `Error` subclasses with a literal `_tag` and typed properties. ## Define an error ```ts import { TaggedError } from "better-result"; class UserNotFound extends TaggedError("UserNotFound")<{ userId: string; message: string; }> {} const error = new UserNotFound({ userId: "usr_123", message: "User usr_123 was not found", }); ``` The class has normal `Error` behavior plus: - `name === "UserNotFound"`; - `_tag === "UserNotFound"` as a string literal; - readonly `userId` and other declared properties; - `toJSON()`; - exhaustive `.match()` by `_tag`; - `UserNotFound.is(value)`; - iterator support for `yield*`. ## Match an error union ```ts const message = error.match({ UserNotFound: (error) => `No user ${error.userId}`, PermissionDenied: (error) => `Missing permission ${error.permission}`, }); ``` The handler map must cover every variant in the receiver's error union. Each handler receives its concrete error subtype. If the selected handler throws, `.match()` throws `Panic` with that exception as its cause. Use the standalone `matchError` function for structurally tagged errors or data-last matching. `match` is reserved for this method. A TaggedError payload property or incompatible subclass member named `match` is rejected by TypeScript. ## Add a computed message ```ts class RequestFailed extends TaggedError("RequestFailed")<{ url: string; status: number; message: string; }> { constructor(args: { url: string; status: number }) { super({ ...args, message: `Request to ${args.url} failed with ${args.status}`, }); } } ``` ## Preserve a cause Declare `cause` in the property type and pass it to `super`: ```ts class ParseFailed extends TaggedError("ParseFailed")<{ input: string; cause: unknown; message: string; }> {} new ParseFailed({ input, cause, message: "Could not parse input" }); ``` Native `Error.cause` is populated. `toJSON()` serializes an `Error` cause to its name, message, and stack. ## Guards ```ts if (UserNotFound.is(value)) { value.userId; } if (TaggedError.is(value)) { value._tag; value.toJSON(); } if (isTaggedError(value)) { value._tag; } ``` `TaggedError.is` and `isTaggedError` detect any better-result tagged error. A concrete class's `.is` guard detects that class. ## Yield directly ```ts const result = Result.gen(function* () { if (!user) { yield* new UserNotFound({ userId, message: "User not found" }); } return Result.ok(user); }); ``` ## Public helper types - `TaggedErrorClass` describes the factory's generic class. - `TaggedErrorInstance` describes an instance structurally. - `AnyTaggedError` describes any tagged error with `toJSON()`. > **Current class syntax** > > The class extends `TaggedError("Tag")` directly. There is no trailing `()` after the property type. --- # Installation Source: https://better-result.dev/getting-started/installation ## Requirements - TypeScript 5.4 or newer is required. - The package is ESM-only. - It has no runtime dependencies and does not require a specific server or browser runtime. ```sh npm npm install better-result ``` ```sh pnpm pnpm add better-result ``` ```sh Bun bun add better-result ``` ```sh Yarn yarn add better-result ``` ## Import values and types ```ts import { Result, TaggedError, matchError, type Result as ResultType, type InferOk, type InferErr, } from "better-result"; ``` `Result` is both the namespace-like object that contains constructors/combinators and the name of the union type. Alias the type to `ResultType` when a file uses both heavily. ```ts const findUser = (id: string): ResultType => { // ... }; ``` ## Runtime support `better-result` uses standard JavaScript classes, generators, async generators, `AbortSignal`, and ESM. Your runtime or build target must support the specific feature you use. Generator composition requires generator support; retry cancellation requires `AbortController`/`AbortSignal`. ## Verify the install ```ts import { Result } from "better-result"; const answer = Result.ok(42); if (answer.status === "ok") { console.log(answer.value); } ``` Run your normal type-checker. No plugin, transform, or global type declaration is required. > **Tree-shaking and package shape** > > The package exposes one ESM entry point. Import public symbols from `better-result`; internal file > paths are not part of the supported API. --- # Mental model Source: https://better-result.dev/getting-started/mental-model A `Result` is a discriminated union: ```ts class Ok { readonly status = "ok"; readonly value: T; } class Err { readonly status = "error"; readonly error: E; } type Result = Ok | Err; ``` The actual classes carry methods and iterator behavior, but these three fields are the heart of the model. ## Expected failures are values Use `Err` when a caller can make a meaningful decision: - input is invalid; - a record is absent; - credentials are rejected; - an upstream service is unavailable; - serialization input does not satisfy a schema. The caller sees `E` in the return type and cannot accidentally use the success value first. ## Defects are not domain errors A callback that unexpectedly throws inside `.map()`, `.andThen()`, `.match()`, recovery, observation, a generator, or codec validation becomes or throws a `Panic`. A defect should be reported and fixed, not silently widened into `E | unknown`. ```ts const value = Result.ok(2).map(() => { throw new Error("broken invariant"); }); // throws Panic ``` See [Panic and defects](/errors/panic-and-defects) for the exact boundary. ## Branches narrow normally ```ts const result: ResultType = findUser(id); if (result.status === "ok") { result.value; // User } else { result.error; // FindUserError } ``` You can also use `Result.isOk`, `Result.isError`, `.isOk()`, `.isErr()`, or `.match()`. ## Transform the branch you own | Operation | Runs on | Purpose | | ------------------ | --------------- | -------------------------------------------- | | `map` | `Ok` | Change a success value | | `mapError` | `Err` | Change an error value | | `andThen` | `Ok` | Continue with another Result-returning step | | `tryRecover` | `Err` | Recover or replace the error | | `tap` / `tapError` | selected branch | Observe without changing the Result | | `match` | both | Leave the Result abstraction with one output | ## Put Result at useful boundaries A useful Result boundary has a caller that can act on the error. Good examples are repositories, parsers, domain operations, adapters, and application workflows. Do not wrap every pure getter or turn programmer mistakes into recoverable variants. > **Design from the caller** > > Start by naming what the caller can do after each failure. That usually reveals the right tagged > error variants and the right boundary. ## Composition preserves evidence `andThen` and `Result.gen` union errors from every step. The final signature is a compact ledger of all expected failures: ```ts const loadDashboard = (): ResultType => Result.gen(function* () { const session = yield* readSession(); const user = yield* findUser(session.userId); const dashboard = yield* queryDashboard(user.id); return Result.ok(dashboard); }); ``` Handle or translate that union at the next meaningful boundary. --- # Quickstart Source: https://better-result.dev/getting-started/quickstart This example parses an environment value, validates it, and builds a server address without throwing expected failures. ## 1. Define errors callers can distinguish ```ts import { Result, TaggedError, type Result as ResultType } from "better-result"; class MissingEnv extends TaggedError("MissingEnv")<{ name: string; message: string; }> {} class InvalidPort extends TaggedError("InvalidPort")<{ input: string; message: string; }> {} ``` The `_tag` field is a string literal, so unions narrow without `instanceof` coupling. ## 2. Return Results from fallible operations ```ts const readEnv = (name: string): ResultType => { const value = process.env[name]; return value === undefined ? Result.err(new MissingEnv({ name, message: `${name} is required` })) : Result.ok(value); }; const parsePort = (input: string): ResultType => { const port = Number(input); return Number.isInteger(port) && port > 0 && port <= 65_535 ? Result.ok(port) : Result.err(new InvalidPort({ input, message: "Expected a port from 1 to 65535" })); }; ``` ## 3. Compose with `yield*` ```ts const readServerAddress = () => Result.gen(function* () { const host = yield* readEnv("HOST"); const portText = yield* readEnv("PORT"); const port = yield* parsePort(portText); return Result.ok(`http://${host}:${port}`); }); // Result ``` The first `Err` short-circuits. Every `Ok` is unwrapped. The error type is inferred from what the generator yielded. ## 4. Handle the complete result ```ts const exitCode = readServerAddress().match({ ok: (address) => { console.log(`Listening at ${address}`); return 0; }, err: (error) => { console.error( error.match({ MissingEnv: (missing) => `Configuration missing: ${missing.name}`, InvalidPort: (invalid) => `Invalid PORT ${JSON.stringify(invalid.input)}`, }), ); return 1; }, }); ``` Adding a new tagged error to this workflow makes the exhaustive `.match()` handlers fail to type-check until you decide how to present it. ## Next steps Learn sync, async, direct tagged-error yields, and cleanup behavior. Add constructors, metadata, JSON output, and guards. --- # Documentation for agents Source: https://better-result.dev/guides/agents This site is built for browser readers and plain-text-searching coding agents from the same source. ## Machine-readable routes | Route | Use | | ------------------------- | -------------------------------- | | `/agents.txt` | Compact documentation index | | `/llms.txt` | Alias-compatible compact index | | `/llms-full.txt` | Full documentation corpus | | `/.md` | Clean Markdown for one page | | `/.mdx` | Original MDX source for one page | | `/agent-readability.json` | Generated readability metadata | Codex probes `/agents.txt` directly, while clients that follow the `llms.txt` convention can use `/llms.txt`. Every HTML page also advertises the compact index with a `rel="alternate"` plain-text link. Point an agent at the smallest source that answers its question. A focused page costs less context than the full corpus. ## Portable skills The repository ships two agent skills: - [`adopt-better-result`](https://github.com/dmmulroy/better-result/tree/3.0/skills/adopt-better-result) audits a TypeScript repository or migrates one named vertical slice. - [`migrate-better-result-3`](https://github.com/dmmulroy/better-result/tree/3.0/skills/migrate-better-result-3) guides migration from older better-result APIs. Install with skills.sh-compatible tooling: ```sh npx skills add dmmulroy/better-result@adopt-better-result npx skills add dmmulroy/better-result@migrate-better-result-3 ``` ## Prompting contract Useful agent instructions name the boundary and desired behavior: ```text Use better-result for the createUser vertical slice. Return tagged domain errors from validation and persistence, compose them with Result.gen, and exhaustively map them to the existing HTTP response contract. Do not convert defects to Err. ``` Avoid asking an agent to “Result-ify everything.” Result boundaries should correspond to caller decisions. ## Search vocabulary Use literal public names in searches and prompts: - `Result.tryPromise` for exceptions, retries, jitter, and abort signals; - `Result.gen` and `Result.await` for generator composition; - `TaggedError#match`, `matchError`, and `matchErrorPartial` for error unions; - `Result.codec` for validated transport/persistence boundaries; `serializeUnsafe` and `deserializeUnsafe` opt into `Panic` for codec validation errors; - `Panic`, `isPanic`, and `panic` for defects; - `Result.all`, `allAsync`, `partition`, and `partitionAsync` for collections. **Agent implementation checklist** 1. Inspect the installed package declarations or repository source before editing. 2. Inventory each expected failure from source to handling boundary. 3. Define tagged error variants in the owning domain vocabulary. 4. Keep unknown exceptions at adapters and translate them with `Result.try` or `Result.tryPromise`. 5. Compose Results without `unwrap()` in ordinary control flow. 6. Handle the complete inferred error union at a policy boundary. 7. Test each branch and run formatting, lint, type-checking, tests, and build. --- # Application patterns Source: https://better-result.dev/guides/application-patterns ## Parse at the edge Turn untrusted input into domain values once: ```ts const parseCreateUser = (input: unknown): ResultType => { const parsed = CreateUserSchema.safeParse(input); return parsed.success ? Result.ok(parsed.data) : Result.err(new InvalidCreateUser({ issues: parsed.error.issues, message: "Invalid user" })); }; ``` Inner code should receive meaningful values instead of repeatedly checking unknown shapes. ## One error vocabulary per boundary A repository may expose `UserNotFound | UserStoreUnavailable`, while its database adapter knows driver-specific exceptions. Translate once: ```ts const findUser = (id: UserId) => queryUserRow(id).mapError((cause) => cause._tag === "NoRows" ? new UserNotFound({ id, message: "User not found" }) : new UserStoreUnavailable({ cause, message: "User store unavailable" }), ); ``` Do not leak framework, database, or HTTP error types through domain APIs. ## Compose in an application workflow ```ts const registerUser = (input: unknown) => Result.gen(function* () { const command = yield* parseCreateUser(input); yield* ensureEmailAvailable(command.email); const user = yield* insertUser(command); yield* publishUserRegistered(user); return Result.ok(user); }); ``` The signature records every expected failure. Handle or normalize the union where the caller's policy lives. ## Map to transport once ```ts const toHttpResponse = (result: ResultType) => result.match({ ok: (user) => Response.json(user, { status: 201 }), err: (error) => error.match({ InvalidCreateUser: (error) => Response.json(error.toJSON(), { status: 400 }), EmailTaken: (error) => Response.json(error.toJSON(), { status: 409 }), UserStoreUnavailable: () => Response.json({ message: "Try again" }, { status: 503 }), PublishFailed: () => Response.json({ message: "Try again" }, { status: 503 }), }), }); ``` ## Avoid common traps Strings are hard to narrow, enrich, serialize deliberately, and match exhaustively. Prefer tagged classes at module boundaries. Pure, total helpers do not need Result. Introduce Result where failure is part of the caller's decision. Panic is a defect signal. Catch it at reporting/supervision boundaries, not in ordinary domain control flow. `unwrap()` converts `Err` into a thrown defect. Compose or handle instead unless the Err truly violates an invariant. --- # Testing Result code Source: https://better-result.dev/guides/testing ## Assert the discriminated shape ```ts const result = parsePort("3000"); expect(result.status).toBe("ok"); if (result.status === "ok") { expect(result.value).toBe(3000); } ``` Narrow before accessing payloads so tests follow the same contract as production callers. ## Test each error variant ```ts const result = parsePort("nope"); expect(result.status).toBe("error"); if (result.status === "error") { expect(InvalidPort.is(result.error)).toBe(true); expect(result.error.input).toBe("nope"); } ``` For a workflow, cover every tagged variant the public signature promises—not only a generic failure assertion. ## Test short-circuit behavior ```ts const save = vi.fn(() => Result.ok(undefined)); const result = Result.gen(function* () { yield* Result.err(new InvalidInput({ message: "invalid" })); yield* save(); return Result.ok(undefined); }); expect(Result.isError(result)).toBe(true); expect(save).not.toHaveBeenCalled(); ``` ## Test retry policy deterministically Inject or faithfully control the operation's sequence. Assert attempt numbers and final Result. Keep delays tiny or use your test runner's fake timers. ```ts const attempts: number[] = []; const result = await Result.tryPromise( async ({ attempt }) => { attempts.push(attempt); if (attempt < 3) throw new Error("temporary"); return "ready"; }, { retry: { times: 2, delayMs: 0, backoff: "constant" } }, ); expect(attempts).toEqual([1, 2, 3]); expect(Result.isOk(result)).toBe(true); ``` ## Assert defects separately ```ts expect(() => Result.ok(1).map(() => { throw new Error("bug"); }), ).toThrow(Panic); ``` Do not make expected-error tests catch Panic; that blurs the contract you are trying to prove. ## Add compile-time tests Inference is part of the API. Use your repository's type-test convention to verify important unions: ```ts const result = Result.all([Result.ok(1), Result.err("failed" as const)] as const); type Error = InferErr; // "failed" ``` --- # Migrate from 2.x Source: https://better-result.dev/migration/from-2 Upgrade the dependency, then let the compiler inventory changed call sites: ```sh bun add better-result@latest bun run check ``` ## TaggedError no longer has a trailing factory call ```ts // Before class NotFound extends TaggedError("NotFound")<{ id: string; message: string }>() {} // Now class NotFound extends TaggedError("NotFound")<{ id: string; message: string }> {} ``` `TaggedErrorClass` becomes `TaggedErrorClass`; payload typing is applied by the subclass. ## Replace unvalidated serialization helpers `Result.serialize`, `Result.deserialize`, and `Result.hydrate` were removed. Define boundary-owned schemas: ```ts const codec = Result.codec({ serialize: { ok: DomainToWireSchema, err: ErrorToWireSchema }, deserialize: { ok: DomainFromWireSchema, err: ErrorFromWireSchema }, }); const encoded = await codec.serialize(result); const decoded = await codec.deserialize(input); ``` Handle `ResultSerializationError` and `ResultDeserializationError`. Schema async behavior controls operation async behavior. When you own both producer and consumer, version their schemas together, and treat contract mismatch as a defect, `serializeUnsafe` and `deserializeUnsafe` remove the codec-error handling layer and throw `Panic` instead. A valid decoded Err remains a domain Err. Keep safe methods at public, independently versioned, persisted, or otherwise untrusted boundaries. ## Recovery can widen success ```ts const result: ResultType = findUser(id); const recovered = result.tryRecover(() => Result.ok(guestId)); // Result ``` Do not cast away the widened union. Narrow it or return a common domain type. ## Matching infers handler return unions `matchError` and `matchErrorPartial` retain different handler return types. `matchErrorPartial` may omit its fallback; unhandled variants then pass through unchanged. ```ts const output = matchErrorPartial(error, { NotFound: () => 404, }); // 404 | unhandled error variants ``` ## TaggedError instance matching Tagged errors can now match their union directly. This is additive; existing `matchError` calls remain valid. ```ts const response = result.match({ ok: (user) => ({ status: 200, body: user }), err: (error) => error.match({ UserNotFound: () => ({ status: 404, body: null }), DatabaseUnavailable: () => ({ status: 503, body: null }), }), }); ``` Use `matchError` for structurally tagged errors or data-last matching. Both exhaustive forms turn a selected handler exception into `Panic` and preserve the exception as `cause`. `match` is now a reserved TaggedError instance name. TypeScript rejects payload properties and incompatible subclass members with that name; rename any collision during migration. ## Collection additions - `Result.all` preserves tuple success types and returns the first error. - `Result.allAsync` awaits concurrently and preserves input-order error selection. - `Result.partition` supports heterogeneous Results. - `Result.partitionAsync` awaits concurrently and partitions both branches. ## Retry additions Existing bounded static retry policies remain valid. Async retries also support: - `TryPromiseContext.signal`; - top-level `signal` cancellation; - dynamic `delayMs(error, context)`; - static `jitter`; - `shouldRetry(error, context)`. Dynamic delays cannot use `backoff` or `jitter`. ## Repository skill For a systematic migration, use the repository's portable `migrate-better-result-3` skill. It inventories affected APIs, includes a safe TaggedError codemod, and treats the compiler as the migration ledger. ```sh npx skills add dmmulroy/better-result@migrate-better-result-3 ``` > **Keep behavior stable first** > > Apply mechanical syntax and codec changes, type-check, then adopt optional collection or retry > features separately. This keeps migration failures attributable. --- # Error API Source: https://better-result.dev/reference/errors ## `TaggedError(tag)` Factory for a generic Error subclass: ```ts class NotFound extends TaggedError("NotFound")<{ id: string; message: string; }> {} ``` Instances expose `_tag`, declared properties, native Error fields, `toJSON()`, exhaustive `.match(handlers)`, and generator iterator behavior. Concrete classes expose `.is(value)`. ## Guards | API | Narrows to | | ------------------------- | --------------------- | | `TaggedError.is(value)` | `AnyTaggedError` | | `isTaggedError(value)` | `AnyTaggedError` | | `ConcreteError.is(value)` | the concrete subclass | | `Panic.is(value)` | `Panic` | | `isPanic(value)` | `Panic` | ## Matching | API | Behavior | | ---------------------------------------------- | ---------------------------------------------------- | | `error.match(handlers)` | Exhaustive instance match over every `_tag` | | `matchError(error, handlers)` | Exhaustive standalone match over every `_tag` | | `matchError(handlers)(error)` | Data-last exhaustive match | | `matchErrorPartial(error, handlers)` | Handle selected tags; return others unchanged | | `matchErrorPartial(error, handlers, fallback)` | Handle selected tags; transform others with fallback | | `matchErrorPartial(handlers, fallback)(error)` | Data-last partial match | Handler return types are inferred as a union when they differ. If an exhaustive `.match()` or `matchError` handler throws, the operation throws `Panic` with the original exception as `cause`. `matchErrorPartial` retains its fallback and handler exception behavior. The `match` property name is reserved on `TaggedError` payloads; TypeScript also rejects incompatible subclass members with that name. ## Built-in error classes ### `UnhandledException` Returned by `Result.try` and `Result.tryPromise` when no custom catch translator is supplied. | Property | Type | | --------- | ---------------------- | | `_tag` | `"UnhandledException"` | | `cause` | `unknown` | | `message` | `string` | ### `ResultSerializationError` Returned when a selected serialization schema reports validation issues. `serializeUnsafe` throws `Panic` with this error as its cause instead of returning it. | Property | Type | | -------- | ---------------------------------------- | | `_tag` | `"ResultSerializationError"` | | `value` | `unknown` | | `issues` | optional readonly Standard Schema issues | ### `ResultDeserializationError` Returned for an invalid serialized envelope or when a selected payload schema reports issues. `deserializeUnsafe` throws `Panic` with this error as its cause while preserving valid decoded domain Err values. | Property | Type | | -------- | ------------------------------------------------------------------------ | | `_tag` | `"ResultDeserializationError"` | | `value` | `unknown` | | `issues` | optional readonly Standard Schema issues; absent for an invalid envelope | ### `Panic` Thrown for defects. Exposes `_tag: "Panic"`, `message`, optional `cause`, `toJSON()`, static `.is`, and generator iterator behavior. ## `panic(message, cause?)` Throws a new `Panic` and returns `never` at the type level. --- # Exported types Source: https://better-result.dev/reference/exported-types All public types are imported from `better-result`. ## Result and inference | Type | Meaning | | ------------------ | ------------------------------------------------- | | `Result` | `Ok \| Err` | | `Ok` | success variant class | | `Err` | error variant class | | `InferOk` | extract the success type from a Result or variant | | `InferErr` | extract the error type from a Result or variant | ```ts const result = Result.gen(function* () { const user = yield* findUser(id); return Result.ok(user.name); }); type Value = InferOk; // string type Error = InferErr; // FindUserError ``` ## Tagged errors | Type | Meaning | | --------------------------------- | ---------------------------------------------------- | | `AnyTaggedError` | any tagged Error with `_tag` and `toJSON()` | | `TaggedErrorClass` | generic class returned by the factory | | `TaggedErrorInstance` | structural tagged error instance with readonly Props | ## Retry context | Type | Fields | | ------------------- | ------------------------------------------------- | | `TryContext` | `attempt: number` (one-based) | | `TryPromiseContext` | `attempt: number`, optional `signal: AbortSignal` | Retry config itself is inferred from `Result.tryPromise`; it is not exported as a named type. ## Serialized envelopes | Type | Shape | | ------------------------ | ------------------------------------- | | `SerializedOk` | `{ status: "ok"; value: T }` | | `SerializedErr` | `{ status: "error"; error: E }` | | `SerializedResult` | `SerializedOk \| SerializedErr` | These describe wire envelopes, not proof of validation. Use `Result.codec` at untrusted boundaries. ## Codec types | Type | Meaning | | ------------------------ | ----------------------------------------------- | | `ResultCodecConfig<...>` | four schemas for serialize/deserialize × ok/err | | `ResultCodec<...>` | safe and unsafe codec operations | | `ResultCodecIssue` | Standard Schema validation issue alias | ## Standard Schema types | Type | Meaning | | --------------------------------- | ----------------------------------- | | `StandardSchemaV1` | supported Standard Schema interface | | `StandardSchemaInput` | infer schema input | | `StandardSchemaOutput` | infer schema output | | `StandardSchemaIssue` | one validation issue | | `StandardSchemaPathSegment` | validation path property wrapper | | `StandardSchemaResult` | schema success-or-issues result | Prefer your schema library's public types in application code. These exports are useful for generic codec tooling and libraries that integrate directly with better-result. --- # Ok and Err API Source: https://better-result.dev/reference/ok-and-err ## `Ok` A successful Result variant. `E` is phantom: it exists for type composition but is not stored at runtime. | Member | Type / behavior | | ---------------------------- | ------------------------------------------------ | | `status` | literal `"ok"` | | `value` | `A` | | `isOk()` | returns true and narrows | | `isErr()` | returns false and narrows | | `map(fn)` | transforms `value` | | `mapError(fn)` | no-op; updates phantom error type | | `andThen(fn)` | runs next Result-returning operation | | `andThenAsync(fn)` | runs async next operation | | `tryRecover(fn)` | no-op; preserves existing success | | `tryRecoverAsync(fn)` | async no-op; preserves existing success | | `match(handlers)` | runs `ok` handler | | `unwrap()` | returns `value` | | `unwrapOr(fallback)` | returns `value` | | `tap` / `tapAsync` | observes success | | `tapError` / `tapErrorAsync` | no-op | | `tapBoth` / `tapBothAsync` | runs `ok` observer | | `[Symbol.iterator]()` | returns `value` to `Result.gen` without yielding | ## `Err` An error Result variant. `T` is phantom: it exists for type composition but is not stored at runtime. | Member | Type / behavior | | ---------------------------- | ------------------------------------------- | | `status` | literal `"error"` | | `error` | `E` | | `isOk()` | returns false and narrows | | `isErr()` | returns true and narrows | | `map(fn)` | no-op; updates phantom success type | | `mapError(fn)` | transforms `error` | | `andThen(fn)` | no-op; preserves existing error | | `andThenAsync(fn)` | async no-op; preserves existing error | | `tryRecover(fn)` | runs recovery | | `tryRecoverAsync(fn)` | runs async recovery | | `match(handlers)` | runs `err` handler | | `unwrap(message?)` | throws `Panic` with `error` as cause | | `unwrapOr(fallback)` | returns fallback | | `tap` / `tapAsync` | no-op | | `tapError` / `tapErrorAsync` | observes error | | `tapBoth` / `tapBothAsync` | runs `err` observer | | `[Symbol.iterator]()` | yields itself to short-circuit `Result.gen` | ## Construct through `Result` ```ts const ok = Result.ok(value); const err = Result.err(error); ``` `Ok` and `Err` classes are exported for guards, type declarations, and advanced use, but the constructors on `Result` communicate intent more clearly. ## Callback contract Instance callbacks are protected. A thrown or rejected callback becomes `Panic`; it is never silently added to the Result's typed error union. --- # Result API Source: https://better-result.dev/reference/result Import the runtime object and union type from the same entry point: ```ts import { Result, type Result as ResultType } from "better-result"; ``` ## Constructors and guards | API | Return | Behavior | | -------------------------------------------- | ---------------------------------------- | ---------------------------------------------------- | | `Result.ok(value?)` | `Ok` | Create success; omitted value is `void` | | `Result.err(error)` | `Err` | Create failure | | `Result.isOk(result)` | type predicate | Narrow to `Ok` | | `Result.isError(result)` | type predicate | Narrow to `Err` | | `Result.try(fn, config?)` | `Result` | Capture a sync exception; optional immediate retries | | `Result.try({ try, catch }, config?)` | `Result` | Capture and translate a sync exception | | `Result.tryPromise(fn, config?)` | `Promise>` | Capture Promise rejection; supports retries | | `Result.tryPromise({ try, catch }, config?)` | `Promise>` | Capture and translate Promise rejection | ## Transformation and composition | API | Runs on | Result | | ------------------------------------ | -------------------- | ----------------------------- | | `Result.map(result, fn)` | Ok | `Result` | | `Result.mapError(result, fn)` | Err | `Result` | | `Result.andThen(result, fn)` | Ok | `Result` | | `Result.andThenAsync(result, fn)` | Ok | `Promise>` | | `Result.tryRecover(result, fn)` | Err | `Result` | | `Result.tryRecoverAsync(result, fn)` | Err | `Promise>` | | `Result.flatten(result)` | Ok containing Result | `Result` | These binary combinators also support data-last calls such as `Result.map(fn)(result)`. ## Handling and extraction | API | Behavior | | ----------------------------------- | ---------------------------------------- | | `Result.match(result, { ok, err })` | Fold both branches into one output type | | `Result.unwrap(result, message?)` | Return Ok value or throw `Panic` for Err | | `Result.unwrapOr(result, fallback)` | Return Ok value or fallback | `match` and `unwrapOr` support data-last calls. `unwrap` is data-first. ## Observation | API | Selected callback | Return | | ---------------------- | ---------------------- | -------------------------- | | `Result.tap` | Ok, sync | original Result | | `Result.tapAsync` | Ok, async | Promise of original Result | | `Result.tapError` | Err, sync | original Result | | `Result.tapErrorAsync` | Err, async | Promise of original Result | | `Result.tapBoth` | branch-specific, sync | original Result | | `Result.tapBothAsync` | branch-specific, async | Promise of original Result | All support data-first and data-last forms. Callback failure throws `Panic`. ## Generators | API | Purpose | | ---------------------------------------- | ------------------------------------------------------------ | | `Result.gen(function* () { ... })` | Compose synchronous Results with `yield*` | | `Result.gen(async function* () { ... })` | Compose sync and async Results | | `Result.await(promise)` | Make `Promise>` yieldable in an async generator | ## Collections | API | Behavior | | -------------------------------- | ---------------------------------------------------------------- | | `Result.all(results)` | Collect Ok values or return first Err | | `Result.allAsync(results)` | Await concurrently, then collect or return first input-order Err | | `Result.partition(results)` | Return `[okValues, errorValues]` | | `Result.partitionAsync(results)` | Await concurrently, then partition | Tuple inputs preserve success positions and union their errors. Async helpers turn rejected input Promises into `Panic`. ## Serialization ```ts const codec = Result.codec({ serialize: { ok: okToWireSchema, err: errorToWireSchema }, deserialize: { ok: okFromWireSchema, err: errorFromWireSchema }, }); ``` | Method | Behavior | | ------------------------- | ---------------------------------------------------------------------- | | `codec.serialize` | Validate an outbound Result and return a Result containing an envelope | | `codec.serializeUnsafe` | Return the envelope or throw `Panic` on serialization failure | | `codec.deserialize` | Validate an envelope and return the decoded Result | | `codec.deserializeUnsafe` | Return the decoded Result or throw `Panic` on deserialization failure | See [Result codecs](/serialization/result-codecs) for sync/async inference and error behavior. --- # Result codecs Source: https://better-result.dev/serialization/result-codecs `Result.codec` builds a two-way boundary from [Standard Schema](https://standardschema.dev/)-compatible schemas. Zod, Valibot, ArkType, and other compliant libraries can provide the validators. ## Why a codec A Result object in memory is not proof that JSON or stored data has the same shape. A codec validates both the envelope and selected payload while allowing in-memory and wire representations to differ. ## Define both directions Define four named schemas at the boundary: success and error schemas for values going to the wire, and success and error schemas for values coming from the wire. Keep each schema's validation and mapping outside the codec declaration. ```ts import { Result } from "better-result"; const UserResultCodec = Result.codec({ serialize: { ok: UserToWireSchema, err: DomainErrorToWireSchema, }, deserialize: { ok: UserFromWireSchema, err: DomainErrorFromWireSchema, }, }); ``` The named schemas may validate identical in-memory and wire shapes or map between different representations such as `Date` and ISO text. `Result.codec` only assembles those four boundary contracts. ## Serialize ```ts const encoded = await UserResultCodec.serialize(Result.ok(user)); // Result, ResultSerializationError> ``` The output envelope is one of: ```ts { status: "ok", value: okPayload } { status: "error", error: errPayload } ``` A schema issue returns `ResultSerializationError` with `value` and `issues`. ## Deserialize unknown input ```ts const decoded = await UserResultCodec.deserialize(inputFromNetwork); // Result ``` An invalid envelope or payload returns `ResultDeserializationError`. Its `value` is the invalid input; schema failures also include `issues`. ## Unsafe convenience methods When you own both producer and consumer and version their schemas together, the unsafe variants are often the simpler choice. A codec validation error then usually means the shared contract is broken, so the unsafe methods remove the codec-error handling layer and its associated unwrapping or translation boilerplate: ```ts const envelope = await UserResultCodec.serializeUnsafe(Result.ok(user)); // SerializedResult const decoded = await UserResultCodec.deserializeUnsafe(inputFromNetwork); // Result ``` `serializeUnsafe` unwraps the serialization Result and panics on `ResultSerializationError`. `deserializeUnsafe` panics only on `ResultDeserializationError`; a valid serialized Err remains a decoded domain Err. Both preserve the selected schema's synchronous or asynchronous behavior. Use the safe methods for public, independently versioned, persisted, or otherwise untrusted boundaries where contract mismatch is an expected condition that the caller should handle. ## Sync and async schemas Each selected schema controls whether that operation returns `Result`, `Promise`, or a union when the branch is unknown. `await` accepts both and is the simplest uniform boundary style: ```ts const decoded = await codec.deserialize(input); ``` A schema that throws or rejects violates Standard Schema validation behavior and causes `Panic`. ## Undefined payloads JSON omits properties with `undefined`. The codec accepts `{ status: "ok" }` and `{ status: "error" }`, then passes `undefined` to the selected payload schema. A void/undefined schema may accept it; other schemas return `ResultDeserializationError`. > **Own codecs at boundaries** > > Define a codec next to the RPC, storage, queue, or server-action boundary it protects. Avoid one > global codec that couples unrelated transports.