@noctuatech/result
v1.1.0
Published
A Rust-like Result implementation for JavaScript and TypeScript that provides a safe way to handle operations that can succeed or fail.
Readme
Result
A Rust-like Result implementation for JavaScript and TypeScript that provides a safe way to handle operations that can succeed or fail.
Table of Contents
- Installation
- Overview
- Core Types
- Ok Class
- Err Class
- Result Namespace
- Utility Functions
- Examples
- Best Practices
Installation
npm install @noctuatech/resultOverview
The Result library provides a type-safe way to handle operations that can either succeed or fail, inspired by Rust's Result type. Instead of throwing exceptions, functions return a Result that can be either Ok(value) or Err(error).
Key Benefits
- Type Safety: Compile-time guarantees about error handling
- No Exceptions: Explicit error handling without try-catch blocks
- Composable: Chain operations with
map,andThen, etc. - Async Support: Built-in support for async operations
- Retry Logic: Built-in retry mechanism with exponential backoff
Core Types
Result<T, E>
A union type representing either success (Ok<T>) or failure (Err<E>).
type Result<T, E> = Ok<T> | Err<E>;Ok<T>
Represents a successful result containing a value of type T.
Err<E>
Represents a failed result containing an error of type E.
Ok Class
The Ok class represents a successful result.
Properties
ok: true
Always true for Ok instances.
err: false
Always false for Ok instances.
val: T
The contained success value.
Methods
unwrap(): T
Returns the contained Ok value.
const result = new Ok(42);
console.log(result.unwrap()); // 42unwrapOr<T2>(val: T2): T | T2
Returns the contained Ok value or a provided default. For Ok instances, always returns the contained value.
const result = new Ok(42);
console.log(result.unwrapOr(0)); // 42expect(msg: string): T
Returns the contained Ok value. For Ok instances, always returns the value without throwing.
const result = new Ok("data");
console.log(result.expect("Should not fail")); // "data"expectErr(msg: string): never
Throws an error with the provided message. This method is designed to be called on Err instances.
const result = new Ok(42);
result.expectErr("This will throw"); // Throws: "This will throw"map<U>(mapper: (val: T) => U): Result<U, E>
Maps a Result<T, E> to Result<U, E> by applying a function to the contained Ok value.
const result = new Ok(5);
const doubled = result.map((x) => x * 2);
console.log(doubled.val); // 10andThen<T2, E2>(mapper: (val: T) => Result<T2, E2>): Result<T2, E | E2>
Calls the mapper function if the result is Ok, otherwise returns the Err value.
const result = new Ok(5);
const chained = result.andThen((x) => new Ok(x * 2));
console.log(chained.unwrap()); // 10
const errorResult = result.andThen(() => new Err("error"));
console.log(errorResult.err); // truemapErr<F>(mapper: (val: E) => F): Result<T, F>
Maps a Result<T, E> to Result<T, F> by applying a function to a contained Err value. For Ok instances, returns the same Ok value.
const result = new Ok(42) as Result<number, string>;
const mapped = result.mapErr((err) => `Error: ${err}`);
console.log(mapped.unwrap()); // 42 (unchanged)toString(): string
Returns a string representation of the Ok value.
const result = new Ok("hello");
console.log(result.toString()); // "Ok(hello)"[Symbol.iterator](): Iterator<T extends Iterable<infer U> ? U : never>
Makes Ok instances iterable when the contained value is iterable.
const result = new Ok([1, 2, 3]);
const values = [...result];
console.log(values); // [1, 2, 3]Err Class
The Err class represents a failed result.
Properties
ok: false
Always false for Err instances.
err: true
Always true for Err instances.
val: E
The contained error value.
attempted?: boolean
Optional flag indicating if this error has been through retry attempts.
stack: string
The stack trace of the error.
Methods
unwrap(): never
Throws an error with the contained error value and stack trace.
const result = new Err("Something went wrong");
result.unwrap(); // Throws: "Tried to unwrap Error: Something went wrong"unwrapOr<T2>(val: T2): T2
Returns the provided default value.
const result = new Err("error");
console.log(result.unwrapOr("default")); // "default"expect(msg: string): never
Throws an error with a custom message and the contained error value.
const result = new Err("network error");
result.expect("Failed to fetch data"); // Throws: "Failed to fetch data - Error: network error"expectErr(msg: string): E
Returns the contained error value.
const result = new Err("validation failed");
console.log(result.expectErr("Should be an error")); // "validation failed"map<U>(mapper: (val: T) => U): Err<E>
Returns the same Err instance without applying the mapper.
const result = new Err("error") as Result<number, string>;
const mapped = result.map((x) => x * 2);
console.log(mapped.err); // true
console.log(mapped.val); // "error"andThen<T2, E2>(mapper: (val: T) => Result<T2, E2>): Err<E>
Returns the same Err instance without calling the mapper.
const result = new Err("error") as Result<number, string>;
const chained = result.andThen((x) => new Ok(x * 2));
console.log(chained.err); // true
console.log(chained.val); // "error"mapErr<F>(mapper: (val: E) => F): Err<F>
Maps the error value using the provided function.
const result = new Err("original error");
const mapped = result.mapErr((err) => `Mapped: ${err}`);
console.log(mapped.val); // "Mapped: original error"toString(): string
Returns a string representation of the Err value.
const result = new Err("error message");
console.log(result.toString()); // "Err(error message)"Result Namespace
The Result namespace provides utility functions for creating and working with Result instances.
Static Methods
Result.ok<T>(val: T): Ok<T>
Creates a new Ok instance with the provided value.
const result = Result.ok(42);
console.log(result.ok); // true
console.log(result.val); // 42Result.err<T>(val: T): Err<T>
Creates a new Err instance with the provided error value.
const result = Result.err("Something went wrong");
console.log(result.err); // true
console.log(result.val); // "Something went wrong"Result.wrap<T, E = unknown>(op: () => T): Result<T, E>
Wraps a function that may throw an error into a Result. Catches any thrown errors and returns them as Err.
// Successful operation
const success = Result.wrap(() => JSON.parse('{"valid": true}'));
console.log(success.ok); // true
// Operation that throws
const failure = Result.wrap(() => JSON.parse("invalid json"));
console.log(failure.err); // true
console.log(failure.val instanceof SyntaxError); // trueResult.wrapAsync<T, E = unknown>(op: () => Promise<T>): Promise<Result<T, E>>
Wraps an async function that may throw or reject into a Result. Catches both synchronous throws and promise rejections.
// Successful async operation
const success = await Result.wrapAsync(async () => {
const response = await fetch("/api/data");
return response.json();
});Result.attempt<T, E>(cb: () => Promise<Result<T, E>>, options?: AttemptOptions): Promise<Result<T, E>>
Attempts to execute a callback function multiple times with retry logic.
Options:
attempts: number(default: 3) - Number of attemptstimeout: number(default: 1000) - Initial timeout in millisecondsbackoff: number(default: 0.5) - Backoff multiplier
async function fetchData() {
return Result.wrapAsync(async () => {
const response = await fetch("/api/data");
return response.json();
});
}
const result = await Result.attempt(fetchData, {
attempts: 5,
timeout: 1000,
backoff: 0.5,
});Result.isResult<T = any, E = any>(val: unknown): val is Result<T, E>
Type guard that checks if a value is a Result instance.
const result = Result.ok(42);
console.log(Result.isResult(result)); // true
console.log(Result.isResult(42)); // false
console.log(Result.isResult("string")); // falseExamples
Basic Usage
import { Ok, Err, Result } from "@noctuatech/result";
// Creating results
const success = Result.ok(42);
const failure = Result.err("Something went wrong");
// Checking results
if (success.ok) {
console.log(success.val); // 42
}
if (failure.err) {
console.error(failure.val); // "Something went wrong"
}Error Handling
// Safe unwrapping
const result = Result.ok(42);
const value = result.unwrapOr(0); // 42
// With error handling
const riskyResult = Result.wrap(() => JSON.parse(userInput));
if (riskyResult.err) {
console.error("Invalid JSON:", riskyResult.val);
} else {
console.log("Parsed data:", riskyResult.val);
}Chaining Operations
const result = Result.ok(5)
.map((x) => x * 2) // 10
.andThen((x) => (x > 10 ? Result.ok(x) : Result.err("Too small")))
.map((x) => x.toString()); // "10"
if (result.ok) {
console.log(result.val); // "10"
}Async Operations
const fetchUser = async (id: number) => {
return Result.wrapAsync(async () => {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
});
};
const userResult = await fetchUser(123);
if (userResult.ok) {
console.log("User:", userResult.val);
} else {
console.error("Failed to fetch user:", userResult.val);
}Retry Logic
async function fetchWithRetry(url: string) {
return Result.attempt(async () => {
return Result.wrapAsync(async () => {
const response = await fetch(url);
if (!response.ok) {
return Result.err(`HTTP ${response.status}`);
}
return Result.ok(await response.json());
});
});
}Best Practices
1. Use Result Instead of Throwing
// ❌ Don't throw
function divide(a: number, b: number): number {
if (b === 0) throw new Error("Division by zero");
return a / b;
}
// ✅ Return Result
function divide(a: number, b: number): Result<number, string> {
if (b === 0) return Result.err("Division by zero");
return Result.ok(a / b);
}2. Handle Errors Explicitly
// ❌ Don't ignore errors
const result = riskyOperation();
const value = result.unwrap(); // Might throw
// ✅ Handle errors properly
const result = riskyOperation();
if (result.ok) {
console.log(result.val);
} else {
console.error("Operation failed:", result.val);
}3. Use map and andThen for Transformations
// ✅ Chain operations
const result = Result.ok(5)
.map((x) => x * 2)
.andThen((x) => (x > 10 ? Result.ok(x) : Result.err("Too small")))
.map((x) => x.toString());4. Use wrap for Exception Safety
// ✅ Wrap potentially throwing functions
const result = Result.wrap(() => JSON.parse(jsonString));
if (result.err) {
// Handle parsing error
}5. Use Type Guards
// ✅ Check result types
if (Result.isResult(someValue)) {
// someValue is definitely a Result
if (someValue.ok) {
// Handle success
}
}