Skip to content

Async results and options

This library ships two awaitable wrappers around Result and Option, so asynchronous code can use the same chainable API:

Both are awaitable, so you can use them like promises:

Getting an async value

typescript
import { Ok } from 'results-ts';

const asyncResult = Ok(1).mapAsync(async (n) => n + 1);
//    ^? AsyncResult<number, never>

const result = await asyncResult; // Ok(2)

A single *Async call is enough to lift a synchronous value into the async world - from there every subsequent chainable method stays async.

Chaining

Most methods on AsyncResult and AsyncOption return another async wrapper, so pipelines chain naturally:

typescript
import { Ok, Err } from 'results-ts';

const loadUser = async (id: number) => {
    if (id === 13) return Err({ code: 'NOT_FOUND', id } as const);

    return Ok({ id, name: 'Ada' });
};

const name = await Ok(1)
    .andThenAsync(loadUser)
    .map((user) => user.name)
    .unwrapOr('anonymous');

Terminal methods that collapse the wrapper to a plain value return a Promise. In the example above, AsyncResult.unwrapOr() returns Promise<string>, so the pipeline is awaited once at the end. AsyncResult.match() works the same way when you need explicit branching.

Panics become rejections

On a Result, the panic methods Result.unwrap(), Result.expect(), Result.unwrapErr(), and Result.expectErr() throw synchronously. On an AsyncResult, their async counterparts return a Promise that rejects with the same internal PanicError instead - prefer AsyncResult.unwrapOr(), AsyncResult.unwrapOrElse(), or AsyncResult.match() over handling the rejections.

typescript
import { Ok, Err } from 'results-ts';

// resolves with the value:
const value = await Ok(21)
    .mapAsync(async (n) => n * 2)
    .unwrap(); // 42

// rejects with PanicError:
await Err('boom')
    .mapAsync(async (n) => n)
    .unwrap();
// => PanicError: called `Result.unwrap()` on an `Err` value

The same applies to AsyncOption's panic methods.

Next steps