Skip to main content

Result<T, E>

Result<T, E> is a discriminated union of Ok<T, E> and Err<T, E>. It represents an operation that can succeed with a value of type T or fail with an error of type E. This is the same concept as Rust's std::result::Result.

Creating Results

Use the ok() and err() constructor functions:

import { ok, err } from "antithrow";

const success = ok(42); // Ok<number, never>
const failure = err("oops"); // Err<never, string>
const empty = ok(); // Ok<void, never>

Type parameters are inferred from the value. When you need to annotate a function's return type, use the Result type alias:

import { ok, err, type Result } from "antithrow";

function parseNumber(s: string): Result<number, string> {
const n = Number(s);
if (Number.isNaN(n)) return err(`Invalid number: ${s}`);
return ok(n);
}

Checking the variant

isOk() and isErr() are type predicates — they narrow the type in if blocks so you can access .value or .error directly:

const result = parseNumber("42");

if (result.isOk()) {
console.log(result.value); // number
}

if (result.isErr()) {
console.error(result.error); // string
}

For combined check-and-test, use isOkAnd() and isErrAnd():

ok(42).isOkAnd((x) => x > 10); // true
ok(5).isOkAnd((x) => x > 10); // false
err("e").isOkAnd(() => true); // false

These also accept type predicates for further narrowing:

const result: Result<string | number, Error> = ok(42);

if (result.isOkAnd((v): v is number => typeof v === "number")) {
result.value; // number (narrowed from string | number)
}

Extracting values

Safe extraction

unwrapOr() and unwrapOrElse() return the Ok value or a fallback:

ok(42).unwrapOr(0); // 42
err("oops").unwrapOr(0); // 0
err("oops").unwrapOrElse((e) => e.length); // 4

Direct property access

After narrowing with isOk() / isErr(), access .value or .error directly — this is the safest and most idiomatic approach:

if (result.isOk()) {
result.value; // T
}
if (result.isErr()) {
result.error; // E
}

Unsafe extraction

unwrap() and expect() throw if the Result is the wrong variant. Use these sparingly — they defeat the purpose of typed errors:

ok(42).unwrap(); // 42
err("oops").unwrap(); // throws!
ok(42).expect("value should exist"); // 42
err("oops").expect("value should exist"); // throws with "value should exist"

Transforming Results

map / mapErr

map() transforms the Ok value, leaving Err unchanged. mapErr() does the reverse:

ok(2).map((x) => x * 2); // ok(4)
err("oops").map((x) => x * 2); // err("oops")

ok(2).mapErr((e) => e.toUpperCase()); // ok(2)
err("oops").mapErr((e) => e.toUpperCase()); // err("OOPS")

Chain them to build transformation pipelines:

parseNumber("42")
.map((n) => n.toFixed(2))
.mapErr((e) => new Error(e));
// Result<string, Error>

mapOr / mapOrElse

Transform and extract in one step:

ok(2).mapOr(0, (x) => x * 2); // 4
err("oops").mapOr(0, (x) => x * 2); // 0

ok(2).mapOrElse(
(e) => e.length,
(x) => x * 2,
); // 4
err("oops").mapOrElse(
(e) => e.length,
(x) => x * 2,
); // 4

Chaining fallible operations

andThen

andThen() is the key method for sequencing operations that can fail. Unlike map(), the callback returns a Result — this lets you chain multiple fallible steps:

function parseNumber(s: string): Result<number, string> {
/* ... */
}
function divide(a: number, b: number): Result<number, string> {
/* ... */
}

parseNumber("10")
.andThen((a) => parseNumber("2").map((b) => [a, b] as const))
.andThen(([a, b]) => divide(a, b));
// Result<number, string>

When you chain andThen with functions that have different error types, TypeScript automatically unions the errors:

declare function parse(s: string): Result<Config, ParseError>;
declare function validate(c: Config): Result<Config, ValidationError>;

parse(input).andThen(validate);
// Result<Config, ParseError | ValidationError>

and / or / orElse

// and: return a fixed result if this is Ok
ok(2).and(ok("next")); // ok("next")
err("a").and(ok("next")); // err("a")

// or: provide a fallback result
ok(2).or(ok(99)); // ok(2)
err("a").or(ok(99)); // ok(99)

// orElse: attempt recovery from the error
err("a").orElse((e) => ok(e.length)); // ok(1)
tip

For complex multi-step chains, consider chain() which provides a more readable generator-based syntax.

Pattern matching

match() calls one of two handlers and returns the result — it's exhaustive, so TypeScript ensures both branches are handled:

const message = result.match({
ok: (value) => `Got ${value}`,
err: (error) => `Failed: ${error}`,
});

Side effects

inspect() and inspectErr() call a function for side effects without changing the Result — useful for logging mid-chain:

parseNumber("42")
.inspect((n) => console.log("parsed:", n))
.map((n) => n * 2)
.inspectErr((e) => console.error("failed:", e));

Flattening

flatten() unwraps a nested Result<Result<U, F>, E> into Result<U, E | F>:

ok(ok(42)).flatten(); // ok(42)
ok(err("inner")).flatten(); // err("inner")
err("outer").flatten(); // err("outer")

Bridging to async

When you need to introduce async operations mid-chain, call toAsync() to convert a Result into a ResultAsync, then continue chaining with async-capable methods:

ok(2).toAsync().map(async (x) => x * 2);
// ResultAsync<number, never>

ok(2).toAsync().andThen(async (x) => ok(x * 2));
// ResultAsync<number, never>

Result.try()

Result.try() wraps a throwing function — if it throws, the error is caught and wrapped in Err:

import { Result } from "antithrow";

Result.try(() => JSON.parse('{"a": 1}')); // ok({ a: 1 })
Result.try(() => JSON.parse("invalid")); // err(SyntaxError)
note

For standard globals like JSON.parse, fetch, atob, etc., prefer the pre-built wrappers in @antithrow/std which provide precise error types.

Combining Results

Result.all() combines multiple Result values into a single Result — like Promise.all, but for Results. If all inputs are Ok, you get a tuple of their values. If any is Err, you get the first error:

import { Result, ok, err } from "antithrow";

const results = Result.all([parseNumber("1"), parseNumber("2"), parseNumber("3")]);
// Result<[number, number, number], string>

results.map(([a, b, c]) => a + b + c); // ok(6)

If any result fails, the first Err is returned:

Result.all([parseNumber("1"), parseNumber("abc"), parseNumber("3")]);
// err("Invalid number: abc")

For async scenarios, use ResultAsync.all() which evaluates all inputs concurrently and accepts both Result and ResultAsync values.