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.
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.
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.
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
place(total: number): void {
if (total <= 0) {
throw new InvalidTotal({ total });
}
}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.
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.
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.
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.
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
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>;| Function | Description |
|---|---|
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
Resultis a discriminated union, not a class: there are no methods, and narrowing onokworks as for any union.combinestops at the first failure in order; it does not collect every error.- A
Resultcarries 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
- Domain Errors, what a failure carries
- Aggregates and Command handlers, which return results
errors-as-values