Skip to content

Instantly share code, notes, and snippets.

@nandordudas
Last active October 14, 2025 09:01
Show Gist options
  • Save nandordudas/7ba83657a05ed0106366d122d42fd044 to your computer and use it in GitHub Desktop.
Save nandordudas/7ba83657a05ed0106366d122d42fd044 to your computer and use it in GitHub Desktop.
function _createError<E extends Error>(reason: unknown): E {
if (reason instanceof Error)
return reason as E
return new Error(String(reason)) as E
}
/**
* Result type for Railway-Oriented Programming pattern.
* Represents either a successful value or an error.
*
* @example
* ```ts
* const result: Result<number> = ok(42)
* const error: Result<number> = err(new Error('Failed'))
* ```
*/
export type Result<T, E extends Error = Error>
= | { data: T, error: null }
| { data: null, error: E }
/**
* Creates a successful Result with data.
*
* @example
* ```ts
* const result = ok(42)
* console.log(result.data) // 42
* ```
*/
export function ok<T>(data: T): Result<T, never> {
return { data, error: null }
}
/**
* Creates an error Result.
*
* @example
* ```ts
* const result = err(new Error('Something went wrong'))
* console.log(result.error.message) // 'Something went wrong'
* ```
*/
export function err<E extends Error>(error: E): Result<never, E> {
return { data: null, error }
}
/**
* Wraps a function call in try-catch and returns a Result.
* Handles both synchronous and asynchronous functions.
*
* @param fn - The function to execute
* @param mapError - Optional function to transform the caught error
*
* @example
* ```ts
* // Sync example
* const result = tryCatch(() => JSON.parse('{"valid": "json"}'))
* // result = { data: { valid: 'json' }, error: null }
*
* const error = tryCatch(() => JSON.parse('invalid'))
* // error = { data: null, error: SyntaxError }
*
* // Async example
* const asyncResult = await tryCatch(async () => {
* const response = await fetch('/api/data')
* return response.json()
* })
*
* // Custom error mapping
* const customError = tryCatch(
* () => riskyOperation(),
* (err) => new CustomError(String(err))
* )
* ```
*/
export function tryCatch<T, E extends Error = Error>(
fn: () => Promise<T>,
mapError?: (reason: unknown) => E
): Promise<Result<T, E>>
export function tryCatch<T, E extends Error = Error>(
fn: () => T,
mapError?: (reason: unknown) => E
): Result<T, E>
export function tryCatch<T, E extends Error = Error>(
fn: () => T | Promise<T>,
mapError = _createError<E>,
): Result<T, E> | Promise<Result<T, E>> {
try {
const result = fn()
if (result instanceof Promise) {
return result
.then(data => ({ data, error: null } as const))
.catch((reason: unknown) => ({ data: null, error: mapError(reason) } as const))
}
return { data: result, error: null }
}
catch (reason: unknown) {
return { data: null, error: mapError(reason) }
}
}
/**
* Unwraps a Result in a generator context, yielding errors and returning data.
*
* Note: The generator yields control when encountering an error. The return
* value after yield is never actually used - run() catches the error before
* the generator continues.
*
* @example
* ```ts
* const result = run(function* () {
* const a = yield* unwrap(ok(5))
* const b = yield* unwrap(err(new Error('Stop here')))
* const c = yield* unwrap(ok(3)) // Never executes
* return a + b + c
* })
* // result = { data: null, error: Error('Stop here') }
* ```
*/
export function unwrap<T, E extends Error = Error>(
result: Result<T, E>,
): Generator<Result<never, E>, T, any> {
return (function* () {
if (result.error !== null) {
yield err(result.error)
throw new Error('Unreachable: error should be caught by run()')
}
return result.data!
})()
}
/**
* Async version of unwrap for use with async generators.
* Unwraps a Result promise in an async generator context.
*
* @example
* ```ts
* const asyncResult1 = Promise.resolve(ok(5))
* const asyncResult2 = Promise.resolve(ok(10))
*
* const final = await run(async function* () {
* const a = yield* unwrapAsync(asyncResult1)
* const b = yield* unwrapAsync(asyncResult2)
* return a + b
* })
* // final = { data: 15, error: null }
* ```
*/
export async function* unwrapAsync<T, E extends Error = Error>(
resultPromise: Promise<Result<T, E>>,
): AsyncGenerator<Result<never, E>, T, any> {
const result = await resultPromise
if (result.error !== null) {
yield err(result.error)
throw new Error('Unreachable: error should be caught by run()')
}
return result.data!
}
/**
* Runs a generator function that uses unwrap/unwrapAsync for error handling.
* Implements Railway-Oriented Programming pattern - stops at first error.
*
* @example
* ```ts
* const divide = (a: number, b: number): Result<number> =>
* b === 0 ? err(new Error('Division by zero')) : ok(a / b)
*
* const result = run(function* () {
* const a = yield* unwrap(ok(10))
* const b = yield* unwrap(ok(2))
* const c = yield* unwrap(divide(a, b))
* return c * 3
* })
* // result = { data: 15, error: null }
*
* const errorResult = run(function* () {
* const a = yield* unwrap(ok(10))
* const b = yield* unwrap(divide(a, 0)) // Error here
* return b * 3 // Never executes
* })
* // errorResult = { data: null, error: Error('Division by zero') }
*
* // Async example
* const asyncResult = await run(async function* () {
* const data = yield* unwrapAsync(fetchData())
* const processed = yield* unwrap(processData(data))
* return processed
* })
*
* // With context binding (this)
* class MyClass {
* value = 42
* async process() {
* return await run(async function* (this: MyClass) {
* const result = yield* unwrapAsync(someAsyncOp())
* return this.value + result
* }, this)
* }
* }
* ```
*/
// Overload: sync generator without context
export function run<T, E extends Error = Error>(
generatorFn: (this: void) => Generator<Result<any, E>, T, any>,
): Result<T, E>
// Overload: sync generator with context
export function run<T, C, E extends Error = Error>(
generatorFn: (this: C) => Generator<Result<any, E>, T, any>,
context: C,
): Result<T, E>
// Overload: async generator without context
export function run<T, E extends Error = Error>(
generatorFn: (this: void) => AsyncGenerator<Result<any, E>, T, any>,
): Promise<Result<T, E>>
// Overload: async generator with context
export function run<T, C, E extends Error = Error>(
generatorFn: (this: C) => AsyncGenerator<Result<any, E>, T, any>,
context: C,
): Promise<Result<T, E>>
// Implementation
// eslint-disable-next-line complexity
export function run<T, E extends Error = Error>(
generatorFn: (this: any) => Generator<Result<any, E>, T, any> | AsyncGenerator<Result<any, E>, T, any>,
context?: any,
): Result<T, E> | Promise<Result<T, E>> {
try {
const iterator = context ? generatorFn.call(context) : generatorFn()
if (Symbol.asyncIterator in iterator) {
return (async () => {
try {
let state = await iterator.next()
while (!state.done) {
const result = state.value
if (result.error !== null)
return { data: null, error: result.error }
state = await iterator.next(result.data as any)
}
return { data: state.value, error: null }
}
catch (reason: unknown) {
return { data: null, error: _createError<E>(reason) }
}
})()
}
let state = iterator.next()
while (!state.done) {
const result = state.value
if (result.error !== null)
return { data: null, error: result.error }
state = iterator.next(result.data as any)
}
return { data: state.value, error: null }
}
catch (reason: unknown) {
return { data: null, error: _createError<E>(reason) }
}
}
/**
* Type guard to check if a Result is successful.
* Narrows the type to allow safe access to data property.
*
* @example
* ```ts
* const result: Result<number> = ok(42)
*
* if (isOk(result)) {
* console.log(result.data) // TypeScript knows data is number
* }
* ```
*/
export function isOk<T, E extends Error = Error>(
result: Result<T, E>,
): result is { data: T, error: null } {
return result.error === null
}
/**
* Type guard to check if a Result is an error.
* Narrows the type to allow safe access to error property.
*
* @example
* ```ts
* const result: Result<number> = err(new Error('Failed'))
*
* if (isErr(result)) {
* console.log(result.error.message) // TypeScript knows error is E
* }
* ```
*/
export function isErr<T, E extends Error = Error>(
result: Result<T, E>,
): result is { data: null, error: E } {
return result.error !== null
}
/**
* Transforms the success value of a Result using a mapping function.
* If the Result is an error, returns the error unchanged.
*
* @example
* ```ts
* const result = ok(5)
* const mapped = map(result, x => x * 2)
* // mapped = { data: 10, error: null }
*
* const error = err(new Error('Failed'))
* const mappedError = map(error, x => x * 2)
* // mappedError = { data: null, error: Error('Failed') }
* ```
*/
export function map<T, U, E extends Error = Error>(
result: Result<T, E>,
fn: (data: T) => U,
): Result<U, E> {
if (result.error !== null)
return err(result.error)
return ok(fn(result.data as T))
}
/**
* Chains Result-returning operations together (also known as bind/flatMap).
* If the first Result is an error, returns that error. Otherwise, applies
* the function to the success value.
*
* @example
* ```ts
* const divide = (a: number, b: number): Result<number> =>
* b === 0 ? err(new Error('Division by zero')) : ok(a / b)
*
* const result1 = ok(10)
* const result2 = flatMap(result1, x => divide(x, 2))
* // result2 = { data: 5, error: null }
*
* const result3 = flatMap(result1, x => divide(x, 0))
* // result3 = { data: null, error: Error('Division by zero') }
* ```
*/
export function flatMap<T, U, E extends Error = Error>(
result: Result<T, E>,
fn: (data: T) => Result<U, E>,
): Result<U, E> {
if (result.error !== null)
return err(result.error)
return fn(result.data as T)
}
/**
* Unwraps a Result, returning the success value or a default value if error.
*
* @example
* ```ts
* const success = ok(42)
* unwrapOr(success, 0) // 42
*
* const failure = err(new Error('Failed'))
* unwrapOr(failure, 0) // 0
* ```
*/
export function unwrapOr<T, E extends Error = Error>(
result: Result<T, E>,
defaultValue: T,
): T {
return result.error !== null ? defaultValue : result.data as T
}
/**
* Unwraps a Result, returning the success value or throwing the error.
* Use this when you want to convert Result-based error handling to
* exception-based error handling.
*
* @throws The error from the Result if it's an error
*
* @example
* ```ts
* const success = ok(42)
* unwrapOrThrow(success) // 42
*
* const failure = err(new Error('Failed'))
* unwrapOrThrow(failure) // throws Error('Failed')
* ```
*/
export function unwrapOrThrow<T, E extends Error = Error>(
result: Result<T, E>,
): T {
if (result.error !== null)
throw result.error
return result.data as T
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment