Skip to content

Result<T, E>

Result is used to represent computations that either succeed with a value (Ok<T>) or fail with an error (Err<E>).

1
2
3
4
5
6
7
import { Ok } from 'holo-fn/result'

const result = new Ok<number, string>(10)
  .map(n => n + 1)
  .unwrapOr(0)

console.log(result) // 11

Methods

map(fn: (value: T) => U): Result<U, E>

Maps over the Ok value. Does nothing for Err.

1
2
3
4
5
6
7
import { Ok, Err } from "holo-fn/result";

const result1 = new Ok(5).map((n) => n * 2);
console.log(result1.unwrapOr(0)); // 10

const result2 = new Err<number, string>("Error").map((n) => n * 2);
console.log(result2.unwrapOr(0)); // 0

mapErr(fn: (err: E) => F): Result<T, F>

Maps over the Err value. Does nothing for Ok.

1
2
3
4
5
6
7
import { Ok, Err } from "holo-fn/result";

const result1 = new Ok(10).mapErr((e) => `Error: ${e}`);
console.log(result1.unwrapOr(0)); // 10

const result2 = new Err("Fail").mapErr((e) => `Mapped error: ${e}`);
console.log(result2.unwrapOr(0)); // 0

chain(fn: (value: T) => Result<U, E>): Result<U, E>

Chains the transformation if the value is Ok. Returns Err otherwise.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { Ok, Err } from "holo-fn/result";

const result1 = new Ok(5)
  .chain((n) => new Ok(n * 2))
  .unwrapOr(0);
console.log(result1); // 10

const result2 = new Err<number, string>("Error")
  .chain((n) => new Ok(n * 2))
  .unwrapOr(0);
console.log(result2); // 0

validate(predicate: (value: T) => boolean, error: E): Result<T, E>

Validates the Ok value based on a predicate. If the predicate returns true, keeps the value. If it returns false, converts to Err with the provided error. Does nothing for Err.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import { Ok, Err } from "holo-fn/result";

const result1 = new Ok(25).validate((n) => n >= 18, 'Must be 18+');
console.log(result1.unwrapOr(0)); // 25

const result2 = new Ok(15).validate((n) => n >= 18, 'Must be 18+');
console.log(result2.isErr()); // true

const result3 = new Err<number, string>('Already failed').validate((n) => n >= 18, 'Must be 18+');
console.log(result3.isErr()); // true (keeps original error)

unwrapOr(defaultValue: T): T

Returns the value of Ok, or the default value for Err.

1
2
3
4
5
6
7
import { Ok, Err } from "holo-fn/result";

const result1 = new Ok(15).unwrapOr(0);
console.log(result1); // 15

const result2 = new Err("Error").unwrapOr(100);
console.log(result2); // 100

isOk(): boolean

Checks if the value is Ok.

1
2
3
4
5
6
7
import { Ok, Err } from "holo-fn/result";

const result1 = new Ok(5);
console.log(result1.isOk()); // true

const result2 = new Err("Error");
console.log(result2.isOk()); // false

isErr(): boolean

Checks if the value is Err.

1
2
3
4
5
6
7
import { Ok, Err } from "holo-fn/result";

const result1 = new Ok(5);
console.log(result1.isErr()); // false

const result2 = new Err("Error");
console.log(result2.isErr()); // true

match<T>(cases: { ok: (value: T) => T; err: (err: E) => T }): T

Matches the value to execute either the ok or err case.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import { Ok, Err } from "holo-fn/result";

const result1 = new Ok(10).match({
  ok: (n) => `Success: ${n}`,
  err: (e) => `Failure: ${e}`,
});
console.log(result1); // "Success: 10"

const result2 = new Err("Error").match({
  ok: (n) => `Success: ${n}`,
  err: (e) => `Failure: ${e}`,
});
console.log(result2); // "Failure: Error"

equals(other: Result<T, E>): boolean

Compares this to another Result, returns false if the values inside are different.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import { Ok, Err } from "holo-fn/result";

const result1 = new Ok(10);
console.log(result1.equals(new Ok(10))); // true
console.log(result1.equals(new Ok(20))); // false
console.log(result1.equals(new Err("Error"))); // false


const result2 = new Err("Error");
console.log(result2.equals(new Err("Error"))); // true
console.log(result1.equals(new Err("Error"))); // false
console.log(result2.equals(new Ok(10))); // false

Helpers

ok<T, E>(value: T): Result<T, E>

Creates an Ok value, representing the success of an operation with a value.

1
2
3
4
import { ok } from 'holo-fn/result';

const resultValue = ok(10);
console.log(resultValue.unwrapOr(0)); // 10

err<T, E>(error: E): Result<T, E>

Creates an Err value, representing a failed operation with an value.

1
2
3
4
import { err } from 'holo-fn/result';

const resultValue = err("Error");
console.log(resultValue.unwrapOr("No error")); // "No error"

fromThrowable(fn, onError?)

Wraps a synchronous function in a Result.

1
2
3
4
5
6
7
import { fromThrowable } from 'holo-fn/result';

const input = '{"name": "John", "age": 30}'

const result = fromThrowable(() => JSON.parse(input), e => 'Invalid JSON')

console.log(result) // _Ok { value: { name: 'John', age: 30 } }
  • Returns Ok<T> if fn() succeeds
  • Returns Err<E> if it throws, using onError if provided

fromPromise(promise, onError?)

Wraps a Promise<T> into a Promise<Result<T, E>>.

1
2
3
4
5
import { fromPromise } from 'holo-fn/result';

const result = await fromPromise(fetch('/api'), e => 'Network error')

console.log(result) // _Err { error: 'Network error' }
  • Resolves to Ok<T> on success
  • Resolves to Err<E> on failure

fromAsync(fn, onError?)

Same as fromPromise, but lazy — receives a function returning a Promise.

1
2
3
4
5
import { fromAsync } from 'holo-fn/result';

const result = await fromAsync(() => fetch('/api'), e => 'Request failed')

console.log(result) // _Err { error: 'Request failed' }
  • Allows deferred execution
  • Handles exceptions from async () => ...

Curried Helpers

map

Curried version of the map function for Result. This allows you to apply a transformation to the Ok value in a more functional style.

1
2
3
4
5
6
7
8
9
import { map, Ok } from 'holo-fn/result';

const result = pipe(
  new Ok(5),
  map((x) => x * 2),
  (res) => res.unwrapOr(0)
);

console.log(result); // 10

mapErr

Curried version of mapErr for Result. This allows handling errors in a more functional composition style.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import { Err, mapErr, Ok, Result } from 'holo-fn/result';

const getValue = (value: string | null): Result<string, string> => {
    if (value === null) {
        return new Err("Value is null");
    }
    return new Ok(value);
}

const result = pipe(
  getValue(null),
  mapErr((e) => `Mapped error: ${e}`),
  (res) => res.unwrapOr("No value")
);

console.log(result); // "No value"

chain

Curried version of chain for Result. This allows you to chain transformations on the Ok value in a functional pipeline.

1
2
3
4
5
6
7
8
9
import { chain, Ok } from 'holo-fn/result';

const result = pipe(
  new Ok(10),
  chain((x) => new Ok(x + 5)),
  (res) => res.unwrapOr(0)
);

console.log(result); // 15

validate

Curried version of validate for Result. This allows filtering/validating values in a functional pipeline with custom error messages.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import { ok, validate, unwrapOr } from 'holo-fn/result';

const validateAge = (age: number) =>
  pipe(
    ok<number, string>(age),
    validate((x) => x >= 0, 'Age cannot be negative'),
    validate((x) => x <= 150, 'Age too high'),
    validate((x) => x >= 18, 'Must be 18+'),
    unwrapOr(0)
  );

console.log(validateAge(25)); // 25
console.log(validateAge(15)); // 0 (fails validation)

Common use cases:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import { fromThrowable, ok, validate } from "holo-fn/result";
import { pipe } from "remeda";

// Validate email format
const validateEmail = (email: string) =>
  pipe(
    ok<string, string>(email),
    validate((s) => s.length > 0, 'Email is required'),
    validate((s) => s.includes('@'), 'Must contain @'),
    validate((s) => s.includes('.'), 'Invalid domain')
  );

console.log(validateEmail('testexample.com').unwrapOr('Invalid'));

// Parse and validate numbers
const parsePositive = (input: string) =>
  pipe(
    fromThrowable(
      () => parseInt(input, 10),
      () => 'Invalid number'
    ),
    validate((n) => !isNaN(n), 'Not a number'),
    validate((n) => n > 0, 'Must be positive')
  );

console.log(parsePositive('42').unwrapOr(0)); // 42
console.log(parsePositive('-5').unwrapOr(0)); // 0

// Validate objects
type User = { name: string; age: number };
const validateUser = (user: User) =>
  pipe(
    ok<User, string>(user),
    validate((u) => u.name.length > 0, 'Name required'),
    validate((u) => u.age >= 18, 'Must be adult')
  );

console.log(validateUser({ name: 'Alice', age: 30 }).unwrapOr({ name: '', age: 0 }));
console.log(validateUser({ name: '', age: 16 }).unwrapOr({ name: '', age: 0 }));

unwrapOr

Curried version of unwrapOr for Result. This provides a cleaner way to unwrap the value in a Result, returning a default value if it's Err.

1
2
3
4
5
6
7
8
import { Ok, unwrapOr } from 'holo-fn/result';

const result = pipe(
  new Ok(42),
  unwrapOr(0)
);

console.log(result); // 42

match

Curried version of match for Result. This allows you to handle both Ok and Err in a functional way, providing a clean way to handle both cases.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { match, Ok } from 'holo-fn/result';

const result = pipe(
  new Ok(10),
  match({
    ok: (v) => `Success: ${v}`,
    err: (e) => `Error: ${e}`
  })
);

console.log(result); // "Success: 10"

equals

Curried version of equals for Result. Compares this to another Result, returns false if the values inside are different.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import { equals, Ok } from 'holo-fn/result';
import { pipe } from 'remeda';

const result1 = pipe(
  new Ok(10),
  equals(new Ok(10)),
);

console.log(result1); // true

const result2 = pipe(
  new Ok(10),
  equals(new Ok(11)),
);

console.log(result2); // false

all

Combines an array of Result values into a single Result. Returns Ok with all values if all are Ok, or Err with all errors if any are Err.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import type { Result } from 'holo-fn';
import { all, err, ok } from 'holo-fn/result';

const result1: Result<number[], unknown[]> = all([ok(1), ok(2), ok(3)]);
console.log(result1.unwrapOr([])); // [1, 2, 3]

const result2 = all([err('Name required'), err('Email invalid'), ok(25)]);
console.log(
  result2.match({
    ok: (v) => v,
    err: (e) => e,
  })
); // ['Name required', 'Email invalid']

const result3 = all([]);
console.log(result3.unwrapOr([])); // []

sequence

Combines an array of Result values into a single Result, stopping at the first error (fail-fast). Returns Ok with all values if all are Ok, or Err with the first error encountered.

Unlike all which collects all errors, sequence returns immediately when it finds the first Err.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import type { Result } from 'holo-fn';
import { err, ok, sequence } from 'holo-fn/result';

const result1: Result<number[], unknown> = sequence([ok(1), ok(2), ok(3)]);
console.log(result1.unwrapOr([])); // [1, 2, 3]

const result2 = sequence([
  ok(1),
  err('First error'),
  err('Second error')
]);
console.log(result2.match({
  ok: (v) => v,
  err: (e) => e
})); // 'First error'

partition

Separates an array of Result values into two groups: successes (oks) and failures (errs). Always processes all items and returns both arrays.

Unlike all and sequence which return a Result, partition returns a plain object with two arrays.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import { err, ok, partition } from 'holo-fn/result';

const results = [
  ok<number, string>(1),
  err<number, string>('error1'),
  ok<number, string>(2),
  err<number, string>('error2'),
  ok<number, string>(3),
];

const { oks, errs } = partition(results);
console.log(oks); // [1, 2, 3]
console.log(errs); // ['error1', 'error2']

const { oks: succeeded, errs: failed } = partition(results);
console.log(`✓ ${succeeded.length} succeeded`);
console.log(`✗ ${failed.length} failed`);
failed.forEach((err) => console.error(err));

Common Patterns

Error handling with specific error types

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { pipe } from 'rambda';
import { err, match, type Result } from 'holo-fn/result';

type ApiError = 'NOT_FOUND' | 'UNAUTHORIZED' | 'SERVER_ERROR';

type User = {
  id: number;
  name: string;
};

const result: Result<User, ApiError> = err('NOT_FOUND');

const message = pipe(
  result,
  match({
    ok: (user) => `Welcome ${user.name}`,
    err: (e) => {
      switch (e) {
        case 'NOT_FOUND':
          return 'Resource not found';
        case 'UNAUTHORIZED':
          return 'Access denied';
        case 'SERVER_ERROR':
          return 'Server error occurred';
      }
    },
  })
);

console.log(message);

Validation pipelines

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import { pipe } from 'rambda';
import { map, ok, validate } from 'holo-fn/result';

const validateAge = (age: number) =>
  pipe(
    ok<number, string>(age),
    validate((n) => n >= 0, 'Age must be positive'),
    validate((n) => n <= 150, 'Age must be realistic'),
    validate((n) => n >= 18, 'Must be 18 or older'),
    map((n) => ({ age: n, isAdult: true }))
  );

console.log(validateAge(25));