diff --git a/.changeset/rich-results-unwrap.md b/.changeset/rich-results-unwrap.md new file mode 100644 index 0000000..243dad8 --- /dev/null +++ b/.changeset/rich-results-unwrap.md @@ -0,0 +1,5 @@ +--- +'@byteslice/result': minor +--- + +Added `unwrap` and `expect` functions for extracting success data from a result. diff --git a/packages/result/README.md b/packages/result/README.md index ef2c6f4..a35d9f2 100644 --- a/packages/result/README.md +++ b/packages/result/README.md @@ -50,7 +50,7 @@ Instead of an operation simply returning a value (indicating success) or throwin ## Overview -`@byteslice/result` provides two exports: +`@byteslice/result` provides the following exports: 1. **`Result`** – A discriminated union type representing either: - **Success**: `{ data: S }` @@ -61,6 +61,9 @@ Instead of an operation simply returning a value (indicating success) or throwin - Catches any thrown exception. - Returns a **success** or **failure** object rather than throwing. +3. **`unwrap`** / **`expect`** – Escape hatches (inspired by Rust) that return the + success `data` directly, or throw if the `Result` is a failure. + This pattern is particularly helpful when you want to **avoid using try/catch** directly in your code, or if you need a standardized way to capture failure details. ## Usage @@ -193,6 +196,42 @@ main() If no `onException` hook is provided, then any thrown exceptions are handled by an internal `ensureError` function. As the name implies, it ensures the `onError` hook receives a valid error. +### Unwrapping: `unwrap` and `expect` + +Inspecting the `failure` property is the safe, type-driven way to consume a `Result`. Occasionally, however, you _know_ an operation should have succeeded and simply want the underlying `data`—treating any failure as an exceptional, program-halting event. + +Borrowing from [Rust's `Result`](https://doc.rust-lang.org/std/result/enum.Result.html), `unwrap` and `expect` provide this escape hatch. + +`unwrap` returns the success `data`, or re-throws the failure's underlying error. + +```ts +import { unwrap, withResult } from '@byteslice/result' + +const result = await withResult( + () => fetchData(), + (error) => error, +) + +// returns the data on success, or throws the underlying error on failure +const data = unwrap(result) +``` + +`expect` behaves the same, but lets you describe _why_ a success was expected. On failure it throws an `Error` with your message, preserving the original error via [cause](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause). + +```ts +import { expect, withResult } from '@byteslice/result' + +const result = await withResult( + () => loadConfig(), + (error) => error, +) + +// throws `Error('config should be present', { cause: })` on failure +const config = expect(result, 'config should be present') +``` + +> ⚠️ Like their Rust counterparts, `unwrap` and `expect` trade type safety for convenience. Reach for them only when a failure genuinely represents an unrecoverable state. + ## Contributing Please see [CONTRIBUTING.md](https://github.com/ByteSliceHQ/byteslice/blob/main/CONTRIBUTE.md) for details. diff --git a/packages/result/src/result.test.ts b/packages/result/src/result.test.ts index fd8beb2..cb9b453 100644 --- a/packages/result/src/result.test.ts +++ b/packages/result/src/result.test.ts @@ -1,5 +1,10 @@ import { describe, expect, it } from 'bun:test' -import { withResult } from './result' +import { + type Result, + expect as expectResult, + unwrap, + withResult, +} from './result' const message = 'uh-oh' const error = new Error(message) @@ -99,3 +104,51 @@ describe('withResult', () => { }) }) }) + +describe('unwrap', () => { + it('should return data for a success', () => { + const result: Result = { data: true } + + expect(unwrap(result)).toBeTrue() + }) + + it('should throw the underlying error for a failure', () => { + const result: Result = { failure: error } + + expect(() => unwrap(result)).toThrow(error) + }) + + it('should throw the underlying error of a custom failure', () => { + const result: Result = { + failure: { error, custom: true }, + } + + expect(() => unwrap(result)).toThrow(error) + }) +}) + +describe('expect', () => { + it('should return data for a success', () => { + const result: Result = { data: true } + + expect(expectResult(result, message)).toBeTrue() + }) + + it('should throw an error with the provided message for a failure', () => { + const result: Result = { failure: error } + + expect(() => expectResult(result, message)).toThrow(message) + }) + + it('should preserve the underlying error as the cause', () => { + const result: Result = { failure: error } + + try { + expectResult(result, message) + expect.unreachable() + } catch (ex) { + expect(ex).toBeInstanceOf(Error) + expect((ex as Error).cause).toBe(error) + } + }) +}) diff --git a/packages/result/src/result.ts b/packages/result/src/result.ts index 262b26d..f4250ef 100644 --- a/packages/result/src/result.ts +++ b/packages/result/src/result.ts @@ -15,6 +15,52 @@ function ensureError(ex: unknown): Error { return ex instanceof Error ? ex : new Error('Something went wrong') } +/** Extracts the underlying error from a failure option. */ +function getError(failure: FailureOption): Error { + return failure instanceof Error ? failure : failure.error +} + +/** Narrows a result to its success state. */ +function isSuccess( + result: Result, +): result is Success { + return result.failure === undefined +} + +/** + * Returns the success data of a result, or throws its underlying error. + * + * Inspired by Rust's `Result::unwrap`, this is an escape hatch for when a + * failure is not expected and should surface as an exception. + */ +export function unwrap( + result: Result, +): S { + if (isSuccess(result)) { + return result.data + } + + throw getError(result.failure) +} + +/** + * Returns the success data of a result, or throws an error with the provided + * message (preserving the original error via `cause`). + * + * Inspired by Rust's `Result::expect`, this is an escape hatch that lets the + * caller describe why a success was expected. + */ +export function expect( + result: Result, + message: string, +): S { + if (isSuccess(result)) { + return result.data + } + + throw new Error(message, { cause: getError(result.failure) }) +} + /** Wraps operation with structured result (success and failure states). */ export async function withResult( operation: () => S | Promise,