Compose asynchronous typed results
Consume Brix's awaitable ResultPromise, adapt application promises at their boundary, and compose typed asynchronous outcomes fluently.
Consume ResultPromise
Asynchronous Brix APIs use ResultPromise<T, E>. Expected failures resolve to Err; unexpected Promise failures still reject.
Await it when you need branch properties:
const result = await contentful.getEntry("page", entryId);
if (result.isErr()) return { error: result.error.tag };
return result.value.fields;
Or chain directly and await a terminal method:
const title = await contentful
.getEntry("page", entryId)
.map((entry) => entry.fields.title)
.tapError((error) => logger.warn({ error }))
.unwrapOr("Untitled");
andThen accepts a callback returning a synchronous Result. andThenAsync accepts Promise<Result>, including another ResultPromise:
const asset = await contentful
.getEntry("page", entryId)
.andThenAsync((entry) => contentful.getAsset(entry.fields.heroAssetId))
.unwrap();
The resulting error type is the union of errors from both operations.
Adapt an application promise
Wrap an application-owned Promise<Result<T, E>> once at its API boundary:
import { Result, resultPromise, type ResultPromise } from "brix/result";
function loadBooking(id: string): ResultPromise<Booking, BookingNotFoundError> {
return resultPromise(
repository
.find(id)
.then((booking) =>
booking ? Result.ok(booking) : Result.err(new BookingNotFoundError({ bookingId: id })),
),
);
}
resultPromise decorates the native promise in place with non-enumerable methods and is idempotent. It does not turn a rejected promise into an Err; catch and model only failures your application deliberately classifies as expected.
ResultPromise operations
The exact Brix signatures are:
type ResultPromise<T, E> = Promise<Result<T, E>> & {
map<U>(fn: (value: T) => U): ResultPromise<U, E>;
mapError<E2>(fn: (error: E) => E2): ResultPromise<T, E2>;
tryRecover<U, E2>(fn: (error: E) => Result<U, E2>): ResultPromise<T | U, E2>;
tryRecoverAsync<U, E2>(fn: (error: E) => Promise<Result<U, E2>>): ResultPromise<T | U, E2>;
andThen<U, E2>(fn: (value: T) => Result<U, E2>): ResultPromise<U, E | E2>;
andThenAsync<U, E2>(fn: (value: T) => Promise<Result<U, E2>>): ResultPromise<U, E | E2>;
tap(fn: (value: T) => void): ResultPromise<T, E>;
tapAsync(fn: (value: T) => Promise<void>): ResultPromise<T, E>;
tapError(fn: (error: E) => void): ResultPromise<T, E>;
tapErrorAsync(fn: (error: E) => Promise<void>): ResultPromise<T, E>;
tapBoth(handlers: { ok: (value: T) => void; err: (error: E) => void }): ResultPromise<T, E>;
tapBothAsync(handlers: {
ok: (value: T) => Promise<void>;
err: (error: E) => Promise<void>;
}): ResultPromise<T, E>;
match<U>(handlers: { ok: (value: T) => U; err: (error: E) => U }): Promise<U>;
unwrap(message?: string): Promise<T>;
unwrapOr<D>(fallback: D): Promise<T | D>;
};
Recovery replaces the original error type while preserving either original or recovered success. Async callbacks are awaited before the chain proceeds. Calling native .then(), .catch(), or .finally() deliberately returns an ordinary Promise and leaves the fluent Result API.
Test both branches
import { expect, it } from "brix/test";
it("reports a missing booking", () => {
const result = findBooking("missing");
expect(result.isErr()).toBe(true);
if (result.isErr()) {
expect(result.error).toMatchObject({ bookingId: "missing" });
}
});
For HTTP request results and retries, continue with API routes. For provider-specific result unions, see the Contentful guide, testing, and API reference.