@openally/result
v3.1.0
Published
Another inspired Rust's Result implementation.
Readme
Result<T, E> lets you encode functions that may fail without throwing: it forces callers to explicitly handle both the success (Ok<T>) and failure (Err<E>) cases instead of relying on try/catch. Option<T> does the same for values that may or may not be present (Some<T> / None), avoiding null/undefined checks scattered across the codebase.
Requirements
- Node.js v24 or higher
Getting Started
This package is available in the Node Package Repository and can be easily installed with npm or yarn.
$ npm i @openally/result
# or
$ yarn add @openally/resultUsage example
import fs from "node:fs";
import {
Ok, Err, Option, wrap, type Result
} from "@openally/result";
interface Config {
host: string;
port: number;
}
function readConfigFile(
path: string
): Result<string, string> {
return fs.existsSync(path) ?
Ok(fs.readFileSync(path, "utf8")) :
Err(`config file not found at "${path}"`);
}
function parseJSON(
raw: string
): Result<Record<string, unknown>, string> {
return wrap<Record<string, unknown>, Error>(() => JSON.parse(raw))
.mapErr((error) => `invalid JSON: ${error.message}`);
}
function toConfig(
record: Record<string, unknown>
): Result<Config, string> {
const { host, port } = record;
return typeof host === "string" && typeof port === "number" ?
Ok({ host, port }) :
Err("config must have a string `host` and a numeric `port`");
}
function configFromEnv(): Result<Config, string> {
return Option.from(process.env.HOST)
.andThen((host) => {
return Option.from(process.env.PORT)
.map((port) => ({ host, port: Number(port) }));
})
.toResult("no config file and no HOST/PORT environment variables set");
}
function loadConfig(path: string): Result<Config, string> {
return readConfigFile(path)
.andThen(parseJSON)
.andTee((record) => console.log("loaded raw config:", record))
.andThen(toConfig)
.orElse(() => configFromEnv());
}
loadConfig("config.json").match(
(config) => console.log(`ready on ${config.host}:${config.port}`),
(error) => {
console.error(`failed to load config: ${error}`);
process.exit(1);
}
);Table of contents
Result API
Constructors: Ok, Err
function Ok<T>(value: T): OkImpl<T>;
function Err<E>(error: E): ErrImpl<E>;Build a successful (Ok) or failed (Err) Result.
const good = Ok(1);
const bad = Err("oops");isOk, isErr
isOk(): boolean;
isErr(): boolean;Narrow a Result<T, E> to its Ok or Err variant. Equivalent to reading the .ok/.err boolean properties directly, provided for API parity/readability in conditionals.
const result = Ok(1);
result.isOk(); // true
result.isErr(); // falseunwrap
Get the value if Ok, throw if Err.
Ok(1).unwrap(); // 1
Err("oops").unwrap(); // Error: Tried to unwrap Error: oopsunwrapOr
Get the value if Ok, fallback to a default value if Err (instead of throwing).
Ok(1).unwrapOr(5); // 1
Err("oops").unwrapOr(5); // 5unwrapOrElse
Same as unwrapOr but using a lazy function for the default value.
Ok(1).unwrapOrElse(() => 5); // 1
Err("oops").unwrapOrElse(() => 5); // 5safeUnwrap
Same as unwrap but only available on Ok (useful for type narrowing when you already know the Result is Ok).
map
Map the value for Ok. Does nothing on Err (use mapErr instead).
Ok(1)
.map((v) => v + 1)
.unwrap(); // 2mapErr
Map the value for Err. Does nothing on Ok (use map instead).
Err(new Error("oops"))
.mapErr((cause) => new Error("oh no", { cause }))
.unwrap();mapOr
Map and unwrap in one step:
- Use the default value for
Err - Use the mapper for
Ok
Ok(1)
.mapOr(1, (val) => val * 2); // 2
Err(new Error("oops"))
.mapOr(1, (val) => val * 2); // 1mapOrElse
Same as mapOr but uses a callback (fed with the error) for the default value.
Err(new Error("oops"))
.mapOrElse(
(err) => err.message,
(val) => val * 2
); // oopsandThen
Similar to Promise.then, chain a Result-returning computation on the Ok value. Short-circuits on Err.
Ok(1)
.andThen((value) => Ok(value + 1))
.unwrap(); // 2This can also be used to turn an Ok into an Err.
orElse
The error-side counterpart of andThen: recover from an Err by returning a new Result. Does nothing on Ok.
Err("oops")
.orElse(() => Ok(5))
.unwrap(); // 5
Ok(1)
.orElse(() => Ok(5))
.unwrap(); // 1 (untouched)andTee
Run a side effect (e.g. logging) on the Ok value without changing the Result. Does nothing on Err.
Ok(1)
.andTee((val) => console.log(`got ${val}`))
.unwrap(); // 1, and logs "got 1"orTee
The Err counterpart of andTee: run a side effect on the error without changing the Result. Does nothing on Ok.
Err("oops")
.orTee((err) => console.error(err)); // logs "oops", Result untouchedandThrough
Run a Result-returning validation on the Ok value, keeping the original Ok value if the validation succeeds, or propagating the new Err if it fails.
Ok(1)
.andThrough((val) => val > 0 ? Ok(val) : Err("must be positive"))
.unwrap(); // 1match
Handle both the Ok and Err cases with dedicated callbacks and return a single value — no need to branch on .ok/.err yourself.
const message = readFile("test.txt").match(
(content) => `read ${content.length} bytes`,
(error) => `failed: ${error}`
);stack (Err only)
Return the Err stack trace (captured at creation time, not available on Ok).
const _e = Err(new Error());
console.log(_e.stack);Option API
Constructors: Some, None
function Some<T>(value: T): SomeImpl<T>;
const None: NoneImpl;const present = Some(1);
const absent = None;Option.from
Collapse a value that may be null/undefined into an Option, without needing to import a separate utility.
function Option.from<T>(val: T | null | undefined): Option<NonNullable<T>>;Option.from(5); // Some(5)
Option.from(null); // None
Option.from(undefined); // Noneunwrap, unwrapOr, unwrapOrElse, safeUnwrap, expect
Same semantics as their Result counterparts, but for presence/absence instead of success/failure. expect(msg) is an alias of unwrap() (kept for Rust-API familiarity).
Some(1).unwrap(); // 1
None.unwrap(); // Error: Tried to unwrap None
None.unwrapOr(5); // 5
None.unwrapOrElse(() => 5); // 5map, mapOr, mapOrElse
Same semantics as their Result counterparts.
Some(1).map((v) => v + 1).unwrap(); // 2
None.mapOr(0, (v) => v + 1); // 0andThen
Chain an Option-returning computation on the value. Short-circuits on None.
Some(1).andThen((v) => (v > 0 ? Some(v) : None));toResult
Convert an Option<T> into a Result<T, E>, using the given error value when None.
Some(1).toResult("missing"); // Ok(1)
None.toResult("missing"); // Err("missing")Utilities
wrap
Wrap an operation that may throw (try/catch style) into a Result.
function wrap<T, E = unknown>(op: () => T): Result<T, E>;const result = wrap(() => JSON.parse(input));wrapAsync
Same as wrap but for an async operation, resolving to Promise<Result<T, E>>.
function wrapAsync<T, E = unknown>(op: () => Promise<T>): Promise<Result<T, E>>;const result = await wrapAsync(() => fetch(url).then((r) => r.json()));isResult
Type guard checking whether a value is a Result (either Ok or Err).
function isResult<T = any, E = any>(val: unknown): val is Result<T, E>;combine
Combine a tuple/array of Results into a single Result. Short-circuits on the first Err encountered; the resulting Ok value preserves each input's type at its tuple position.
function combine<T extends readonly Result<any, any>[]>(
results: T
): Result<{ [K in keyof T]: InferOkTypes<T[K]> }, InferErrTypes<T[number]>>;combine([Ok(1), Ok("foo"), Ok(true)]); // Ok([1, "foo", true])
combine([Ok(1), Err("oops"), Err("never reached")]); // Err("oops")combineWithAllErrors
Same as combine, but collects every encountered Err instead of short-circuiting on the first one.
function combineWithAllErrors<T extends readonly Result<any, any>[]>(
results: T
): Result<{ [K in keyof T]: InferOkTypes<T[K]> }, InferErrTypes<T[number]>[]>;combineWithAllErrors([Ok(1), Err("first"), Err("second")]); // Err(["first", "second"])License
MIT
