A lightweight, zero-dependency TypeScript library that brings Rust's Result type to your JavaScript/TypeScript projects. Handle errors gracefully with type safety and functional programming patterns. πΈβ¨
npm install @jenova-marie/ts-rust-result
# or
yarn add @jenova-marie/ts-rust-result
# or
pnpm add @jenova-marie/ts-rust-result
Supports both ESM and CommonJS! Works in all Node.js environments:
import { ok, err } from '@jenova-marie/ts-rust-result'const { ok, err } = require('@jenova-marie/ts-rust-result')tsx, ts-node, and other TS runnersVersion 2.2 adds dual module support and domain-specific helper utilities:
import) and CommonJS (require)createDomainResult<E>() - Eliminate ALL type assertions in your moduleserr<ConfigError>() or as Result<T, E> casts neededimport { ok, err, type Result } from '@jenova-marie/ts-rust-result'
import { fileNotFound, type ConfigError } from './errors.js'
function loadConfig(path: string): Result<Config, ConfigError> {
if (!exists(path)) {
return err<ConfigError>(fileNotFound(path)) // β Still need generic
}
return ok(config) as Result<Config, ConfigError> // β Type assertion
}
// config-result.ts - create once
import { createDomainResult } from '@jenova-marie/ts-rust-result/helpers'
import { type ConfigError } from './errors.js'
export const { ok, err } = createDomainResult<ConfigError>()
export type ConfigResult<T> = Result<T, ConfigError>
// config-loader.ts - use everywhere
import { ok, err, type ConfigResult } from './config-result.js'
import { fileNotFound } from './errors.js'
function loadConfig(path: string): ConfigResult<Config> {
if (!exists(path)) {
return err(fileNotFound(path)) // β
Completely clean!
}
return ok(config) // β
No casts needed!
}
// Recursive functions work perfectly
function processTree(node: Node): ConfigResult<ProcessedNode> {
for (const child of node.children) {
const result = processTree(child)
if (!result.ok) return result // β
Type flows through!
}
return ok(processed)
}
Learn More: See content/PATTERNS.md for comprehensive examples!
Version 2.1 adds full generic type support - the #1 requested feature! No more as any casts:
Result<T, E = Error> with full type inferenceerr(fileNotFound(path)) just works - fully typed!Result<T, FileError | ValidationError> supportedfunction loadConfig(path: string): Result<Config> {
if (!exists(path)) {
return err(fileNotFound(path) as any) // β Type cast required
}
return ok(config)
}
import { type FileNotFoundError } from '@jenova-marie/ts-rust-result/errors'
function loadConfig(path: string): Result<Config, FileNotFoundError> {
if (!exists(path)) {
return err(fileNotFound(path)) // β
Fully typed!
}
return ok(config)
}
// Consumer code gets full type safety
const result = loadConfig('config.json')
if (!result.ok) {
console.log(result.error.path) // β
TypeScript knows .path exists!
}
Migration: Additive change with default parameters - existing code works unchanged!
Version 2.0 transforms ts-rust-result into an opinionated error handling framework with:
error('FileNotFound').withContext({...}).build()fileNotFound(), invalidJSON(), schemaValidation().withCause()fromZodSafeParse() for seamless schema validation@jenova-marie/ts-rust-result/errors, /observabilityMigration from 1.x: The core Result API is unchanged! All 1.x code continues to work. The new error infrastructure is opt-in through separate imports.
Want the unopinionated 1.x version? It remains available at @jenova-marie/ts-rust-result@1.3.6.
// Core Result (unchanged from 1.x)
import { ok, err, type Result } from '@jenova-marie/ts-rust-result'
// New: Opinionated error handling
import { fileNotFound, error, type FileNotFoundError } from '@jenova-marie/ts-rust-result/errors'
import { toLogContext } from '@jenova-marie/ts-rust-result/observability'
function loadConfig(path: string): Result<Config, FileNotFoundError> {
if (!exists(path)) {
return err(fileNotFound(path))
}
return ok(parse(readFile(path)))
}
const result = loadConfig('/app/config.json')
if (!result.ok) {
// Structured logging with one line
logger.error(toLogContext(result.error), 'Failed to load config')
// Send to Sentry
Sentry.captureException(toSentryError(result.error))
// Record metric
errorCounter.inc(toMetricLabels(result.error))
}
Learn More:
Error handling in JavaScript and TypeScript is fundamentally broken. Here's what we're dealing with: π
Inconsistent Error Handling π - Some functions throw exceptions, others return null/undefined, and others return error objects. There's no standard way to handle failures.
Type Safety Issues π± - TypeScript can't guarantee that you've handled all error cases. A function might return User | null, but TypeScript won't force you to check for null.
Error Propagation Hell π₯ - You end up with deeply nested try-catch blocks or error checking at every level of your call stack.
Lost Context π’ - When errors bubble up through multiple layers, you lose the original context and stack trace information.
Unpredictable Control Flow πͺοΈ - Exceptions can be thrown from anywhere, making it hard to reason about your code's execution path.
RustResult provides a consistent, type-safe, and ergonomic way to handle errors by treating them as values rather than exceptions. This approach:
Instead of error-prone traditional patterns with inconsistent error handling, you get a clean, type-safe approach where TypeScript forces you to handle both success and error cases explicitly.
Ok<T> and Err<E> with full TypeScript supportmap, mapErr, unwrap, and moretryResult for wrapping async operationsassert, assertOr, assertNotNil with Result returnsmap, mapErr, and other functional utilitiesimport { ok, err, type Result } from '@jenova-marie/ts-rust-result'
// Functions that can fail return Result<T, E>
function parseNumber(input: string): Result<number> {
const num = Number(input)
if (isNaN(num)) {
return err(new Error(`Invalid number: ${input}`))
}
return ok(num)
}
// Handle results with type-safe checks
const result = parseNumber('42')
if (result.ok) {
console.log(`Parsed: ${result.value}`) // TypeScript knows .value exists
} else {
console.error(result.error.message) // TypeScript knows .error exists
}
// v2.1.0+ with generic error types
type Ok<T> = { ok: true; value: T };
type Err<E = Error> = { ok: false; error: E };
type Result<T, E = Error> = Ok<T> | Err<E>;
ok<T>(value: T): Result<T> πΈCreates a successful result.
err<E>(error: E): Result<never, E> πCreates an error result. v2.1.0+: Fully typed - no casts needed!
isOk<T, E>(result: Result<T, E>): result is Ok<T> β
Type guard to check if a result is successful.
isErr<T, E>(result: Result<T, E>): result is Err<E> βType guard to check if a result is an error.
unwrap<T, E>(result: Result<T, E>): T πUnwraps a result, throwing the error if it's an error.
map<T, U, E>(result: Result<T, E>, fn: (value: T) => U): Result<U, E> πΊοΈMaps a successful result value using the provided function.
mapErr<T, E, F>(result: Result<T, E>, fn: (err: E) => F): Result<T, F> πMaps an error result using the provided function.
tryResult<T, E>(fn: () => Promise<T>, shouldThrow?: boolean): Promise<Result<T, E>> πWraps an async function in a try-catch block and returns a Result.
assert<E>(condition: boolean, error?: E, shouldThrow?: boolean): Result<true, E> β
Rust-style assertion that returns a Result instead of throwing.
assertOr<E>(condition: boolean, error: E, shouldThrow?: boolean): Result<true, E> π―Rust-style assertion with a typed error parameter.
assertNotNil<T, E>(value: T | null | undefined, message?: string, shouldThrow?: boolean): Result<NonNullable<T>, E> πAsserts that a value is not null or undefined, returning the value if valid.
RustResult follows a specific pattern to maintain clean separation between error handling and business logic:
Functions that can fail should implement appropriate error handling and return Result<T> directly, using ok() for success and err() for failures.
When calling functions that might throw (like third-party APIs, database calls, or existing code), wrap the call with tryResult().
If you find yourself wrapping your own functions in tryResult(), you're doing it wrong.
Result<T> directly β¨tryResult() to wrap πtryResult() π«import { ok, err, tryResult, type Result } from '@jenova-marie/ts-rust-result'
import { networkError, type NetworkError } from '@jenova-marie/ts-rust-result/errors'
interface User {
id: string
name: string
}
async function fetchUser(id: string): Promise<Result<User, NetworkError>> {
// Wrap third-party calls that might throw
const response = await tryResult(() => fetch(`/api/users/${id}`))
if (!response.ok) {
return err(networkError(`Failed to fetch user ${id}`, response.error))
}
const data = await tryResult(() => response.value.json())
if (!data.ok) {
return err(networkError('Failed to parse response', data.error))
}
return ok(data.value as User)
}
import { ok, err, type Result } from '@jenova-marie/ts-rust-result'
import { schemaValidation, type SchemaValidationError } from '@jenova-marie/ts-rust-result/errors'
interface UserInput {
email: string
age: number
}
function validateUser(input: UserInput): Result<UserInput, SchemaValidationError> {
const issues: string[] = []
if (!input.email.includes('@')) {
issues.push('Invalid email format')
}
if (input.age < 0 || input.age > 150) {
issues.push('Age must be between 0 and 150')
}
if (issues.length > 0) {
return err(schemaValidation('UserInput', issues))
}
return ok(input)
}
mapErr() πimport { mapErr } from '@jenova-marie/ts-rust-result'
// Transform errors across module boundaries
function loadApplicationConfig(): Result<Config, AppError> {
const result = readConfigFile('/etc/app/config.json')
// Transform FileNotFoundError β AppError with context
const transformed = mapErr(result, (fsErr) =>
appError(`Config missing at ${fsErr.path}: ${fsErr.message}`)
)
if (!transformed.ok) return transformed
return parseConfig(transformed.value)
}
Migrate from traditional try-catch patterns to Result-based error handling for better type safety and consistency.
Convert Promise-based error handling to Result patterns for more predictable control flow.
We love contributions! Here's how you can help:
git clone https://github.com/yourusername/ts-rust-result.gitpnpm installgit checkout -b feature/amazing-featurepnpm buildpnpm testpnpm test:watchpnpm lintpnpm testgit commit -m "feat: add amazing feature"git push origin feature/amazing-featureThis project is committed to providing a welcoming and inclusive environment for all contributors. We expect all participants to:
ts-rust-resultQ: Why not just use try-catch everywhere? π€ A: Try-catch doesn't provide type safety and can make control flow unpredictable. Results make error handling explicit and type-safe.
Q: Isn't this just more verbose? π A: Initially yes, but it prevents bugs and makes your code more maintainable in the long run.
Q: Can I mix Results with traditional error handling? π
A: Yes! Use tryResult to wrap existing async functions and gradually migrate your codebase.
Pattern Guide - Common patterns, best practices, and real-world examples
Error Design Philosophy - Architectural decisions and design patterns
Sentry Integration - Error monitoring with Sentry
OpenTelemetry Integration - Distributed tracing with OpenTelemetry
Zod Integration - Schema validation with Zod
GPL-3.0 License - see the LICENSE file for details.
Made with π by Pippa β¨π¦
"Error handling should be elegant, not an afterthought." πΈβοΈ