Skip to main content

chain()

Generator-based composition for Result and ResultAsync operations. See the concept guide for a detailed tutorial.

Signatures

Synchronous

function chain<T, E>(generator: () => SyncChainGenerator<T, E>): Result<T, E>;

Takes a sync generator function. yield* on a Result unwraps the Ok value or short-circuits on Err. The return value is wrapped in Ok.

import { chain, ok, err } from "antithrow";

const result = chain(function* () {
const a = yield* ok(1);
const b = yield* ok(2);
return a + b;
});
// ok(3)

Asynchronous

function chain<T, E>(generator: () => AsyncChainGenerator<T, E>): ResultAsync<T, E>;

Takes an async generator function. yield* on a ResultAsync awaits and unwraps the Ok value or short-circuits on Err.

import { chain, okAsync } from "antithrow";

const result = await chain(async function* () {
const a = yield* okAsync(1);
const b = yield* okAsync(2);
return a + b;
});
// ok(3)

Types

SyncChainGenerator

type SyncChainGenerator<T, E> = Generator<Err<never, E>, T, void>;

The generator type produced by sync chain() callbacks.

AsyncChainGenerator

type AsyncChainGenerator<T, E> = AsyncGenerator<Err<never, E>, T, void>;

The generator type produced by async chain() callbacks.

Behavior

Inside the generatorWhat happens
yield* ok(value)Returns value, execution continues
yield* err(error)Generator exits, chain() returns err(error)
yield* someResultUnwraps if Ok, exits if Err
return valueGenerator completes, chain() returns ok(value)
throw errorNot caught — propagates as a regular exception
warning

Thrown exceptions from the generator body are not converted into Err. Wrap throwable logic with Result.try() or ResultAsync.try() before yielding.

Examples

Sync: multi-step validation

function validateOrder(order: Order): Result<Order, string> {
return chain(function* () {
if (order.items.length === 0) {
return yield* err("Order has no items");
}

const subtotal = order.items.reduce((sum, item) => sum + item.price * item.quantity, 0);

if (order.discount < 0 || order.discount > 1) {
return yield* err("Invalid discount");
}

return { ...order, total: subtotal * (1 - order.discount) };
});
}

Async: combining sync validation with async operations

import { chain, errAsync, type ResultAsync } from "antithrow";

function createUser(input: CreateUserInput): ResultAsync<User, ApiError> {
return chain(async function* () {
const validatedInput = yield* validateInput(input);
const emailExists = yield* checkEmailExists(validatedInput.email);

if (emailExists) {
return yield* errAsync<User, ApiError>({
type: "validation",
message: "Email already exists",
});
}

return yield* saveUser(validatedInput);
});
}

Mixing yield* and await

In an async chain, use yield* for Result/ResultAsync values and regular await for non-Result promises:

const result = await chain(async function* () {
const user = yield* fetchUser(1); // ResultAsync → yield*
const orders = yield* fetchOrders(user.id); // ResultAsync → yield*

const processed = await Promise.all(
// Promise → await
orders.map(async (order) => ({
orderId: order.id,
status: "processed",
})),
);

return { user: user.name, processed };
});