Skip to content

Result ​

A Result is the outcome of an operation that can fail for a business reason: either ok with a value, or err with a domain error. Failures are returned, never thrown, so every signature lists what can go wrong and TypeScript makes the caller handle it.

ts
function place(total: number): Result<void, InvalidTotal> {
	return total > 0 ? ok() : err(new InvalidTotal({ total }));
}

When to use ​

Return a Result from every operation that can fail in a way the business expects: an invalid amount, an order already placed, a product not found. Business methods of aggregates, factories of value objects, domain services and command handlers all return one.

Keep exceptions for what nobody expects: a bug, a broken invariant, a lost database connection.

Usage ​

Return a success or a failure ​

ok(value) and err(error) build the two cases. ok() without argument is the success of an operation that returns nothing.

src/ordering/domain/aggregates/order.aggregate.ts
ts
place(total: number, eventId: string, now: Date): Result<void, InvalidTotal | OrderAlreadyPlaced> {
	if (this.isPlaced) {
		return err(new OrderAlreadyPlaced());
	}
	if (total <= 0) {
		return err(new InvalidTotal({ total }));
	}
	this.placedTotal = total;
	this.record(new OrderPlaced({ aggregateId: this.id, id: eventId, occurredAt: now, payload: { total } }));
	return ok();
}

Read a result ​

A Result is a plain object with an ok flag. Checking it narrows the type: after if (!result.ok), TypeScript knows result.error; after it, result.value.

src/ordering/application/commands/place-order.command.ts
ts
const placed = order.place(total, this.ids.next(), this.clock.now());
if (!placed.ok) {
	return placed;
}
await this.orders.save(order);
return ok();

Returning the failure as is keeps its type: the handler's error union includes the aggregate's.

Return errors instead of throwing them ​

❌ Avoid
ts
place(total: number): void {
	if (total <= 0) {
		throw new InvalidTotal({ total });
	}
}
✅ Prefer
ts
place(total: number): Result<void, InvalidTotal> {
	if (total <= 0) {
		return err(new InvalidTotal({ total }));
	}
	return ok();
}
Why?

Nothing in place(total: number): void tells the caller it may fail, so it forgets. With a Result, the failure is part of the type and cannot be ignored silently. alveolus arch check reports a thrown domain error and a public aggregate method that returns no Result (errors-as-values).

Transform the value or the error ​

map changes the value of a success, mapErr the error of a failure; the other case passes through untouched.

ts
const total = map(Money.create(amount, currency), (money) => money.amount);
const http = mapErr(placed, (error) => ({ status: 422, code: error.type }));

Chain operations that can fail ​

andThen runs the next step only when the previous one succeeded, and collects both error types.

src/ordering/domain/value-objects/money.value-object.ts
ts
static parse(amount: number, code: string): Result<Money, UnknownCurrency | NegativeAmount> {
	return andThen(Currency.create(code), (currency) => Money.create(amount, currency));
}

Combine several results ​

combine takes an array or an object of results. It returns all the values in the same shape, or the first failure.

ts
const address = combine({ city: City.create(input.city), street: Street.create(input.street), zip: ZipCode.create(input.zip) });
if (!address.ok) {
	return address;
}
address.value.city;

const pair = combine([Money.create(10, eur), Money.create(20, eur)]);

Map a result to a response ​

At the edge, a driving adapter turns the failure into whatever its transport expects.

src/ordering/driving/http/controllers/orders.controller.ts
ts
const placed = await this.placeOrder.handle({ orderId, total: body.total });
if (!placed.ok) {
	return { status: 422, body: { error: placed.error.type, details: placed.error.payload } };
}
return { status: 204 };

Reference ​

ts
type Result<T, E> = Ok<T> | Err<E>;

interface Ok<T> { readonly ok: true; readonly value: T }
interface Err<E> { readonly ok: false; readonly error: E }

function ok(): Ok<void>;
function ok<T>(value: T): Ok<T>;
function err<E>(error: E): Err<E>;
function map<T, E, U>(result: Result<T, E>, transform: (value: T) => U): Result<U, E>;
function mapErr<T, E, F>(result: Result<T, E>, transform: (error: E) => F): Result<T, F>;
function andThen<T, E, U, F>(result: Result<T, E>, next: (value: T) => Result<U, F>): Result<U, E | F>;
function combine(results: readonly Result[] | Record<string, Result>): Result<values in the same shape, first error>;
FunctionDescription
ok() / ok(value)A success, with no value or with one.
err(error)A failure.
map(result, transform)Transforms the value of a success.
mapErr(result, transform)Transforms the error of a failure.
andThen(result, next)Runs next on the value of a success; its errors add to the union.
combine(results)All the values, as an array or an object, or the first failure.

Caveats

  • Result is a discriminated union, not a class: there are no methods, and narrowing on ok works as for any union.
  • combine stops at the first failure in order; it does not collect every error.
  • A Result carries expected failures. Technical failures, such as a lost connection, are thrown and handled like any other exception.

Import from @alveolus/core or @alveolus/core/result.

See also ​

Released under the MIT License.