Domain errors
A domain error is an expected business failure: a total that is not positive, an order already placed. It is a value, returned in a Result, never thrown, so every caller sees in the signature what can go wrong.
export class InvalidTotal extends DomainError<{ total: number }> {}
return err(new InvalidTotal({ total }));When to use
Declare a domain error for every way a business operation can be refused by the rules. Keep exceptions for what should never happen: a bug, a broken invariant, a lost database connection. Those are thrown as plain Errors.
Usage
Declare an error
One class per failure, one file per class, extending DomainError with the type of its payload. The class has no body.
import { DomainError } from "@alveolus/core";
export class InvalidTotal extends DomainError<{ total: number }> {}An error without data takes no type parameter, and no argument:
import { DomainError } from "@alveolus/core";
export class OrderAlreadyPlaced extends DomainError {}new InvalidTotal({ total: 0 });
new OrderAlreadyPlaced();Return it, never throw it
place(total: number): void {
if (total <= 0) {
throw new InvalidTotal({ total });
}
}place(total: number, eventId: string, now: Date): Result<void, InvalidTotal> {
if (total <= 0) {
return err(new InvalidTotal({ total }));
}
…
return ok();
}A DomainError is not an Error: it has no stack trace and is not meant to be thrown. Throwing one is reported by errors-as-values.
List the errors of a use case
A method returns the union of the errors it can produce. A command handler declares the union of everything it may return, and passes failures through unchanged.
export type PlaceOrderError = OrderNotFound | InvalidTotal | OrderAlreadyPlaced;
export class PlaceOrderHandler extends CommandHandler<PlaceOrder, void, PlaceOrderError> { … }Handle it at the edge
The driving adapter turns the error into a response. instanceof narrows the union; type gives the class name, and payload the data.
if (!placed.ok) {
const body = { error: placed.error.type, details: placed.error.payload };
if (placed.error instanceof OrderNotFound) {
return { status: 404, body };
}
return { status: 422, body };
}Compare errors in tests
Errors are plain objects: compare them with toEqual.
expect(order.place(0, "event_1", now)).toEqual(err(new InvalidTotal({ total: 0 })));Reference
abstract class DomainError<Payload = undefined>| Type parameter | Description |
|---|---|
Payload | The data describing the failure. Defaults to undefined: no data. |
| Member | Type | Description |
|---|---|---|
constructor(payload) | public | Takes the payload, or no argument when Payload is undefined. |
payload | Payload | The data of the failure. |
type | string | The class name, such as "InvalidTotal". |
AnyDomainError is the type of any domain error; handlers only accept errors of this type.
Caveats
typeis the class name: a bundler that minifies class names changes it. Keep class names in your build (keep_classnamesin Terser,keepNamesin esbuild), or narrow withinstanceof.- A
DomainErrordoes not extendError. In the domain and the application,class X extends Erroris reported bybuilding-blocks-only: a bug is thrown asnew Error("…").
Import from @alveolus/core or @alveolus/core/domain-errors.
Troubleshooting
Expected 0 arguments, but got 1 or Expected 1 arguments, but got 0: the arguments do not match the payload type. An error declared without a type parameter takes no argument; one declared with a payload requires it.
See also
- Result, how errors are returned and combined
- Aggregates and Value objects, which return them
- Command handlers, which declare them
- Rules:
errors-as-values,building-blocks-only