Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/rich-results-unwrap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@byteslice/result': minor
---

Added `unwrap` and `expect` functions for extracting success data from a result.
41 changes: 40 additions & 1 deletion packages/result/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 }`
Expand All @@ -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
Expand Down Expand Up @@ -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: <original error> })` 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.
Expand Down
55 changes: 54 additions & 1 deletion packages/result/src/result.test.ts
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -99,3 +104,51 @@ describe('withResult', () => {
})
})
})

describe('unwrap', () => {
it('should return data for a success', () => {
const result: Result<boolean> = { data: true }

expect(unwrap(result)).toBeTrue()
})

it('should throw the underlying error for a failure', () => {
const result: Result<boolean> = { failure: error }

expect(() => unwrap(result)).toThrow(error)
})

it('should throw the underlying error of a custom failure', () => {
const result: Result<boolean, { error: Error; custom: boolean }> = {
failure: { error, custom: true },
}

expect(() => unwrap(result)).toThrow(error)
})
})

describe('expect', () => {
it('should return data for a success', () => {
const result: Result<boolean> = { data: true }

expect(expectResult(result, message)).toBeTrue()
})

it('should throw an error with the provided message for a failure', () => {
const result: Result<boolean> = { failure: error }

expect(() => expectResult(result, message)).toThrow(message)
})

it('should preserve the underlying error as the cause', () => {
const result: Result<boolean> = { failure: error }

try {
expectResult(result, message)
expect.unreachable()
} catch (ex) {
expect(ex).toBeInstanceOf(Error)
expect((ex as Error).cause).toBe(error)
}
})
})
46 changes: 46 additions & 0 deletions packages/result/src/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<S, F extends FailureOption>(
result: Result<S, F>,
): result is Success<S> {
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<S, F extends FailureOption = Error>(
result: Result<S, F>,
): 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<S, F extends FailureOption = Error>(
result: Result<S, F>,
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<S, F extends FailureOption = Error>(
operation: () => S | Promise<S>,
Expand Down
Loading